From 10ef677a0d58544f61661c862a3377d976083444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Zago=C5=BEen?= Date: Thu, 28 Aug 2025 13:08:31 +0200 Subject: [PATCH 1/3] Add xml.Node.__repr__() The repr of an xml.Node only includes arguments where the value differs from the default. Do not print positional arg name in repr(xml.Node) "tag" is a non-optional positional argument, it makes for cleaner repr() output if we also omit the name. --- base/src/xml.act | 21 +++++++++++++++++++++ test/stdlib_tests/src/test_xml.act | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/base/src/xml.act b/base/src/xml.act index 30c96f333..97ac2c5e7 100644 --- a/base/src/xml.act +++ b/base/src/xml.act @@ -23,6 +23,27 @@ n.text and c.tail for all children of n. def encode(self) -> str: NotImplemented + def __repr__(self): + # Build the arguments to xml.Node() and skip arguments where our value + # equals the default. We do this with appending to a string because that + # does not leak the *mut* effect, unlike appending to a list. + args = repr(self.tag) + if len(self.nsdefs) != 0: + args += ", nsdefs={repr(self.nsdefs)}" + if self.prefix is not None: + args += ", prefix={repr(self.prefix)}" + if len(self.attributes) != 0: + args += ", attributes={repr(self.attributes)}" + if len(self.children) != 0: + args += ", children={repr(self.children)}" + if self.text is not None: + args += ", text={repr(self.text)}" + if self.tail is not None: + args += ", tail={repr(self.tail)}" + + return "xml.Node({args})" + + class XmlParseError(ValueError): """Exception raised for XML parsing errors diff --git a/test/stdlib_tests/src/test_xml.act b/test/stdlib_tests/src/test_xml.act index 9c349a967..a3d089092 100644 --- a/test/stdlib_tests/src/test_xml.act +++ b/test/stdlib_tests/src/test_xml.act @@ -178,3 +178,8 @@ def _test_xml_namespace_attributes_undefined(): testing.assertEqual(child_attr.1, 'child', "Undefined namespace: child attribute value not preserved") e = xml.encode(d) testing.assertEqual(e, test_xml, "Undefined namespace: roundtrip failed") + +def _test_xml_repr(): + test_xml = """text""" + d = xml.decode(test_xml) + testing.assertEqual(repr(d), "xml.Node('data', nsdefs=[(None, 'http://default')], children=[xml.Node('a', nsdefs=[('ns', 'http://foo')], attributes=[('ns:operation', 'remove')], children=[xml.Node('b', prefix='ns', attributes=[('ns:type', 'element')], text='text')])])") From 8f863073c03bcdd90eabb3b3eb5d3fe8f04011c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Zago=C5=BEen?= Date: Thu, 28 Aug 2025 13:13:05 +0200 Subject: [PATCH 2/3] Do not set optional xml.Node attrs to empty string --- base/src/xml.ext.c | 7 ++++--- test/stdlib_tests/src/test_xml.act | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/base/src/xml.ext.c b/base/src/xml.ext.c index 29e7beac3..b6765de94 100644 --- a/base/src/xml.ext.c +++ b/base/src/xml.ext.c @@ -69,13 +69,14 @@ static unsigned char* copy_with_xml_escape(unsigned char *dst, B_str src, int es } // Helper function to collect text from consecutive TEXT and CDATA nodes -// Returns the combined string and updates the node pointer to the first non-text node +// Returns the combined string (or NULL if there is no text/CDATA content) and +// updates the node pointer to the first non-text node // Note: cur_ptr is passed by reference (pointer to pointer) so we can update the caller's pointer // to skip past all consumed text/CDATA nodes static B_str collect_text_cdata_nodes(xmlNodePtr *cur_ptr) { xmlNodePtr cur = *cur_ptr; if (!cur || (cur->type != XML_TEXT_NODE && cur->type != XML_CDATA_SECTION_NODE)) { - return to$str(""); + return NULL; } // Count total length of combined text and CDATA nodes @@ -106,7 +107,7 @@ static B_str collect_text_cdata_nodes(xmlNodePtr *cur_ptr) { } *cur_ptr = cur; - return to$str(""); + return NULL; } xmlQ_Node $NodePtr2Node(xmlNodePtr node) { diff --git a/test/stdlib_tests/src/test_xml.act b/test/stdlib_tests/src/test_xml.act index a3d089092..9454c31bf 100644 --- a/test/stdlib_tests/src/test_xml.act +++ b/test/stdlib_tests/src/test_xml.act @@ -182,4 +182,4 @@ def _test_xml_namespace_attributes_undefined(): def _test_xml_repr(): test_xml = """text""" d = xml.decode(test_xml) - testing.assertEqual(repr(d), "xml.Node('data', nsdefs=[(None, 'http://default')], children=[xml.Node('a', nsdefs=[('ns', 'http://foo')], attributes=[('ns:operation', 'remove')], children=[xml.Node('b', prefix='ns', attributes=[('ns:type', 'element')], text='text')])])") + testing.assertEqual(repr(d), "xml.Node(tag='data', nsdefs=[(None, 'http://default')], children=[xml.Node(tag='a', nsdefs=[('ns', 'http://foo')], attributes=[('ns:operation', 'remove')], children=[xml.Node(tag='b', prefix='ns', attributes=[('ns:type', 'element')], text='text')])])") From 6541ead364de404c7fd38e34e8f434fa4a452300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Zago=C5=BEen?= Date: Thu, 28 Aug 2025 13:30:32 +0200 Subject: [PATCH 3/3] Add xml.Node.__str__() alias for .encode() --- base/src/xml.act | 3 +++ 1 file changed, 3 insertions(+) diff --git a/base/src/xml.act b/base/src/xml.act index 97ac2c5e7..eabca084c 100644 --- a/base/src/xml.act +++ b/base/src/xml.act @@ -23,6 +23,9 @@ n.text and c.tail for all children of n. def encode(self) -> str: NotImplemented + def __str__(self): + return self.encode() + def __repr__(self): # Build the arguments to xml.Node() and skip arguments where our value # equals the default. We do this with appending to a string because that