Skip to content

Commit 4f1b957

Browse files
Handle unbound XML prefixes when strict=False (#35)
1 parent b6627e8 commit 4f1b957

3 files changed

Lines changed: 69 additions & 1 deletion

File tree

changes/35.feature

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Handle unbound XML prefixes when strict=False
2+
3+
Allow for unbound XML prefixes when parsing with strict=False.
4+
This is useful for handling XML documents that may have missing
5+
namespace declarations.

didl_lite/didl_lite.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""DIDL-Lite (Digital Item Declaration Language) tools for Python."""
33
# pylint: disable=too-many-lines
44

5+
import re
56
from typing import (
67
Any,
78
Dict,
@@ -1073,7 +1074,27 @@ def to_xml_string(*objects: DidlObject) -> bytes:
10731074
def from_xml_string(
10741075
xml_string: str, strict: bool = True
10751076
) -> List[Union[DidlObject, Descriptor]]:
1076-
"""Convert XML string to DIDL Objects."""
1077+
"""Parse DIDL-Lite XML string."""
1078+
if not strict:
1079+
# Find all prefixes used in tags, e.g., <prefix:tag ...>
1080+
used_prefixes = set(re.findall(r"<([a-zA-Z0-9]+):", xml_string))
1081+
1082+
# Find all defined namespaces, e.g., xmlns:prefix=...
1083+
defined_prefixes = set(re.findall(r"xmlns:([a-zA-Z0-9]+)=", xml_string))
1084+
1085+
# Identify prefixes used but not defined.
1086+
missing_prefixes = (
1087+
used_prefixes - defined_prefixes - {"DIDL-Lite", "dc", "upnp", "dlna"}
1088+
)
1089+
1090+
# Remove the "if missing_prefixes:" line and just keep the for loop
1091+
for prefix in missing_prefixes:
1092+
dlna_ns = 'xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/"'
1093+
if dlna_ns in xml_string:
1094+
replacement = f'{dlna_ns} xmlns:{prefix}="http://tempuri.org/{prefix}/"'
1095+
xml_string = xml_string.replace(dlna_ns, replacement)
1096+
1097+
# Proceed with parsing using the (potentially) patched xml_string
10771098
xml_el = defusedxml.ElementTree.fromstring(xml_string)
10781099
return from_xml_el(xml_el, strict)
10791100

tests/test_didl_lite.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,3 +680,45 @@ def test_item_improper_class_nesting(self) -> None:
680680

681681
item = items[0]
682682
assert isinstance(item, didl_lite.MusicTrack)
683+
684+
def test_from_xml_string_unbound_prefix(self) -> None:
685+
"""Test from_xml_string with unbound namespace prefix."""
686+
# The key fix is adding: xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
687+
broken_xml = (
688+
'<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" '
689+
'xmlns:dc="http://purl.org/dc/elements/1.1/" '
690+
'xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" '
691+
'xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">'
692+
'<item id="1" parentID="0" restricted="1">'
693+
"<dc:title>Test Title</dc:title>"
694+
"<song:subTitle>Test Subtitle</song:subTitle>"
695+
"<upnp:class>object.item.audioItem.musicTrack</upnp:class>"
696+
"</item>"
697+
"</DIDL-Lite>"
698+
)
699+
700+
# This call should not throw ParseError when strict=False.
701+
objs = didl_lite.from_xml_string(broken_xml, strict=False)
702+
703+
assert len(objs) == 1
704+
assert objs[0].title == "Test Title"
705+
# Check that the temporary namespace is correctly assigned.
706+
assert "sub_title" in objs[0].__dict__
707+
assert objs[0].sub_title == "Test Subtitle"
708+
assert isinstance(objs[0], didl_lite.MusicTrack)
709+
710+
def test_music_track_artist_and_genre(self) -> None:
711+
"""Test MusicTrack artist and genre properties."""
712+
track = didl_lite.MusicTrack(
713+
id="1",
714+
parent_id="0",
715+
title="Test",
716+
restricted="0",
717+
artist="My Artist",
718+
genre="My Genre",
719+
)
720+
721+
xml = didl_lite.to_xml_string(track)
722+
723+
assert b"<upnp:artist>My Artist</upnp:artist>" in xml
724+
assert b"<upnp:genre>My Genre</upnp:genre>" in xml

0 commit comments

Comments
 (0)