Skip to content

Commit ab4c86f

Browse files
committed
fix(unparse): skip empty lists to keep pretty/compact outputs consistent
1 parent 220240c commit ab4c86f

File tree

2 files changed

+125
-0
lines changed

2 files changed

+125
-0
lines changed

tests/test_dicttoxml.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,126 @@ def test_rejects_names_with_quotes_and_equals(self):
337337
for prefix in ['a"b', "a'b", "a=b"]:
338338
with self.assertRaises(ValueError):
339339
unparse({"a": {"@xmlns": {prefix: "http://e/"}}}, full_document=False)
340+
341+
def test_pretty_print_and_short_empty_elements_consistency(self):
342+
"""Test that pretty and compact modes produce equivalent results when stripped.
343+
344+
This test covers issue #352: Edge case with pretty_print and short_empty_elements.
345+
When short_empty_elements=True, empty elements should be written as <tag/>
346+
regardless of whether pretty printing is enabled.
347+
"""
348+
# Test case from issue #352: empty list child
349+
input_dict = {"Foos": {"Foo": []}}
350+
351+
compact = unparse(
352+
input_dict, pretty=False, short_empty_elements=True, full_document=False
353+
)
354+
pretty = unparse(
355+
input_dict, pretty=True, short_empty_elements=True, full_document=False
356+
)
357+
pretty_compacted = pretty.replace("\n", "").replace("\t", "")
358+
359+
# They should be equal when pretty formatting is stripped
360+
self.assertEqual(pretty_compacted, compact)
361+
self.assertEqual(compact, "<Foos/>")
362+
self.assertEqual(pretty_compacted, "<Foos/>")
363+
364+
def test_empty_list_filtering(self):
365+
"""Test that empty lists are filtered out and don't create empty child elements."""
366+
# Test various cases with empty lists
367+
test_cases = [
368+
# Case 1: Single empty list child
369+
({"Foos": {"Foo": []}}, "<Foos/>"),
370+
# Case 2: Multiple empty list children
371+
({"Foos": {"Foo": [], "Bar": []}}, "<Foos/>"),
372+
# Case 3: Mixed empty and non-empty children
373+
({"Foos": {"Foo": [], "Bar": "value"}}, "<Foos><Bar>value</Bar></Foos>"),
374+
# Case 4: Nested empty lists
375+
({"Foos": {"Foo": {"Bar": []}}}, "<Foos><Foo/></Foos>"),
376+
# Case 5: Empty list with attributes
377+
({"Foos": {"@attr": "value", "Foo": []}}, '<Foos attr="value"/>'),
378+
]
379+
380+
for input_dict, expected_compact in test_cases:
381+
with self.subTest(input_dict=input_dict):
382+
# Test compact mode
383+
compact = unparse(
384+
input_dict,
385+
pretty=False,
386+
short_empty_elements=True,
387+
full_document=False,
388+
)
389+
self.assertEqual(compact, expected_compact)
390+
391+
# Test pretty mode
392+
pretty = unparse(
393+
input_dict,
394+
pretty=True,
395+
short_empty_elements=True,
396+
full_document=False,
397+
)
398+
pretty_compacted = pretty.replace("\n", "").replace("\t", "")
399+
self.assertEqual(pretty_compacted, expected_compact)
400+
401+
def test_empty_list_filtering_with_short_empty_elements_false(self):
402+
"""Test that empty lists are still filtered when short_empty_elements=False."""
403+
input_dict = {"Foos": {"Foo": []}}
404+
405+
# With short_empty_elements=False, empty elements should be <tag></tag>
406+
compact = unparse(
407+
input_dict, pretty=False, short_empty_elements=False, full_document=False
408+
)
409+
pretty = unparse(
410+
input_dict, pretty=True, short_empty_elements=False, full_document=False
411+
)
412+
pretty_compacted = pretty.replace("\n", "").replace("\t", "")
413+
414+
# They should be equal when pretty formatting is stripped
415+
self.assertEqual(pretty_compacted, compact)
416+
self.assertEqual(compact, "<Foos></Foos>")
417+
self.assertEqual(pretty_compacted, "<Foos></Foos>")
418+
419+
def test_non_empty_lists_are_not_filtered(self):
420+
"""Test that non-empty lists are not filtered out."""
421+
# Test with non-empty lists
422+
input_dict = {"Foos": {"Foo": ["item1", "item2"]}}
423+
424+
compact = unparse(
425+
input_dict, pretty=False, short_empty_elements=True, full_document=False
426+
)
427+
pretty = unparse(
428+
input_dict, pretty=True, short_empty_elements=True, full_document=False
429+
)
430+
pretty_compacted = pretty.replace("\n", "").replace("\t", "")
431+
432+
# The lists should be processed normally
433+
self.assertEqual(pretty_compacted, compact)
434+
self.assertEqual(compact, "<Foos><Foo>item1</Foo><Foo>item2</Foo></Foos>")
435+
self.assertEqual(
436+
pretty_compacted, "<Foos><Foo>item1</Foo><Foo>item2</Foo></Foos>"
437+
)
438+
439+
def test_empty_dict_vs_empty_list_behavior(self):
440+
"""Test the difference between empty dicts and empty lists."""
441+
# Empty dict should create a child element
442+
input_dict_dict = {"Foos": {"Foo": {}}}
443+
compact_dict = unparse(
444+
input_dict_dict,
445+
pretty=False,
446+
short_empty_elements=True,
447+
full_document=False,
448+
)
449+
self.assertEqual(compact_dict, "<Foos><Foo/></Foos>")
450+
451+
# Empty list should be filtered out
452+
input_dict_list = {"Foos": {"Foo": []}}
453+
compact_list = unparse(
454+
input_dict_list,
455+
pretty=False,
456+
short_empty_elements=True,
457+
full_document=False,
458+
)
459+
self.assertEqual(compact_list, "<Foos/>")
460+
461+
# They should be different
462+
self.assertNotEqual(compact_dict, compact_list)

xmltodict.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,8 @@ def _emit(key, value, content_handler,
494494
_validate_name(attr_name, "attribute")
495495
attrs[attr_name] = iv
496496
continue
497+
if isinstance(iv, list) and not iv:
498+
continue # Skip empty lists to avoid creating empty child elements
497499
children.append((ik, iv))
498500
if isinstance(indent, int):
499501
indent = ' ' * indent

0 commit comments

Comments
 (0)