From 7ee592c91b1255cb8f01ff87a7eb821bae2a6728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 15 Jul 2026 08:55:29 +0200 Subject: [PATCH 1/3] Fix ClassificationElement parsing of non-numeric class codes from_etree converted the element text to Decimal, which crashes on alphanumeric classification codes and breaks roundtripping since the value is a code, not a number. --- drafthorse/models/elements.py | 2 +- tests/test_elements.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/test_elements.py diff --git a/drafthorse/models/elements.py b/drafthorse/models/elements.py index 31a0e3d..b6e8782 100644 --- a/drafthorse/models/elements.py +++ b/drafthorse/models/elements.py @@ -236,7 +236,7 @@ def to_etree(self): return node def from_etree(self, root, strict=True): - self._text = Decimal(root.text) + self._text = root.text self._list_id = root.attrib["listID"] self._list_version_id = root.attrib["listVersionID"] self._set_on_input = True diff --git a/tests/test_elements.py b/tests/test_elements.py new file mode 100644 index 0000000..7acd13c --- /dev/null +++ b/tests/test_elements.py @@ -0,0 +1,22 @@ +import xml.etree.cElementTree as ET +from datetime import datetime + +import pytest + +from drafthorse.models import NS_RAM +from drafthorse.models.container import IDContainer +from drafthorse.models.elements import ClassificationElement, DirectDateTimeElement + + +def test_classification_element_roundtrip_non_numeric_text(): + el = ClassificationElement(NS_RAM, "ClassCode") + el._text = "ABC123" + el._list_id = "TST" + el._list_version_id = "1" + node = el.to_etree() + + parsed = ClassificationElement(NS_RAM, "ClassCode").from_etree(node) + assert parsed._text == "ABC123" + assert parsed._list_id == "TST" + assert parsed._list_version_id == "1" + From 5c553c26de505b1c04f73d2cdde1eb30c536fd84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 15 Jul 2026 08:55:45 +0200 Subject: [PATCH 2/3] Preserve time and report parse errors in DirectDateTimeElement from_etree dropped the time component via .date() even though to_etree serializes with %H:%M:%S, so roundtrips silently lost data. It also swallowed all parse errors and stored an empty string instead of failing in strict mode. --- drafthorse/models/elements.py | 8 +++++--- tests/test_elements.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/drafthorse/models/elements.py b/drafthorse/models/elements.py index b6e8782..81efca7 100644 --- a/drafthorse/models/elements.py +++ b/drafthorse/models/elements.py @@ -389,9 +389,11 @@ def to_etree(self): def from_etree(self, root, strict=True): try: - self._value = datetime.strptime(root.text, "%Y-%m-%dT%H:%M:%S").date() - except Exception: - self._value = "" + self._value = datetime.strptime(root.text, "%Y-%m-%dT%H:%M:%S") + except ValueError: + if strict: + raise + self._value = None self._set_on_input = True return self diff --git a/tests/test_elements.py b/tests/test_elements.py index 7acd13c..b2746cc 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -20,3 +20,19 @@ def test_classification_element_roundtrip_non_numeric_text(): assert parsed._list_id == "TST" assert parsed._list_version_id == "1" + +def test_direct_datetime_roundtrip_keeps_time(): + el = DirectDateTimeElement(NS_RAM, "CreationDateTime") + el._value = datetime(2024, 5, 3, 14, 30, 45) + node = el.to_etree() + assert node.text == "2024-05-03T14:30:45" + + parsed = DirectDateTimeElement(NS_RAM, "CreationDateTime").from_etree(node) + assert parsed._value == datetime(2024, 5, 3, 14, 30, 45) + + +def test_direct_datetime_invalid_raises_in_strict_mode(): + node = ET.Element("{%s}CreationDateTime" % NS_RAM) + node.text = "not-a-date" + with pytest.raises(ValueError): + DirectDateTimeElement(NS_RAM, "CreationDateTime").from_etree(node, strict=True) From ffb0a4d5c3d74c8df1b84d6cfa773a82d599bbe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 15 Jul 2026 08:55:55 +0200 Subject: [PATCH 3/3] Handle missing schemeID attribute in IDContainer schemeID is optional on ID elements, but add_from_etree accessed the attribute directly and raised KeyError when parsing documents without it. --- drafthorse/models/container.py | 4 ++-- tests/test_elements.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/drafthorse/models/container.py b/drafthorse/models/container.py index 30cdfe3..56f36dd 100644 --- a/drafthorse/models/container.py +++ b/drafthorse/models/container.py @@ -80,10 +80,10 @@ def empty_element(self): def set_element(self, el, child): el._text = child[1] - el._scheme_id = child[0] + el._scheme_id = child[0] or "" def add_from_etree(self, root, strict=True): - self.add((root.attrib["schemeID"], root.text)) + self.add((root.attrib.get("schemeID"), root.text)) class StringContainer(SimpleContainer): diff --git a/tests/test_elements.py b/tests/test_elements.py index b2746cc..d0e0d8a 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -36,3 +36,11 @@ def test_direct_datetime_invalid_raises_in_strict_mode(): node.text = "not-a-date" with pytest.raises(ValueError): DirectDateTimeElement(NS_RAM, "CreationDateTime").from_etree(node, strict=True) + + +def test_id_container_accepts_missing_scheme_id(): + node = ET.Element("{%s}GlobalID" % NS_RAM) + node.text = "4000001123452" + container = IDContainer(child_type=(tuple, list), namespace=NS_RAM, tag="GlobalID") + container.add_from_etree(node) + assert container.children == [(None, "4000001123452")]