From 70ca796dec0b9dbc3908d7b5fe819c077cfcdc9f Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Fri, 17 Jul 2026 23:21:36 -0700 Subject: [PATCH 1/3] fix: serialize text nodes in PyQuery str/html contents() includes text nodes, but __str__/__html__ always passed nodes to lxml tostring, raising TypeError on valid selections. --- pyquery/pyquery.py | 23 ++++++++++++++++++----- tests/test_pyquery.py | 13 +++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/pyquery/pyquery.py b/pyquery/pyquery.py index 53aeaac..f5e7ef1 100644 --- a/pyquery/pyquery.py +++ b/pyquery/pyquery.py @@ -353,12 +353,21 @@ def __str__(self): """ - return ''.join([etree.tostring(e, encoding=str) for e in self]) + has_text_nodes = any(isinstance(e, str) for e in self) + return ''.join( + e if isinstance(e, str) else etree.tostring( + e, encoding=str, with_tail=not has_text_nodes) + for e in self + ) def __unicode__(self): """xml representation of current nodes""" - return u''.join([etree.tostring(e, encoding=str) - for e in self]) + has_text_nodes = any(isinstance(e, str) for e in self) + return u''.join( + e if isinstance(e, str) else etree.tostring( + e, encoding=str, with_tail=not has_text_nodes) + for e in self + ) def __html__(self): """html representation of current nodes:: @@ -369,8 +378,12 @@ def __html__(self): """ - return u''.join([lxml.html.tostring(e, encoding=str) - for e in self]) + has_text_nodes = any(isinstance(e, str) for e in self) + return u''.join( + e if isinstance(e, str) else lxml.html.tostring( + e, encoding=str, with_tail=not has_text_nodes) + for e in self + ) def __repr__(self): r = [] diff --git a/tests/test_pyquery.py b/tests/test_pyquery.py index b8ee3c8..868a60d 100644 --- a/tests/test_pyquery.py +++ b/tests/test_pyquery.py @@ -359,6 +359,19 @@ def test_comment(self): self.assertEqual(doc.text(), 'bar') +class TestContentsStr(TestCase): + + def test_str_contents_with_text_nodes(self): + doc = pq('hello bold world') + contents = doc.contents() + self.assertEqual(str(contents), 'hello bold world') + self.assertEqual(contents.__html__(), 'hello bold world') + + def test_str_contents_text_only(self): + doc = pq('
only text
') + self.assertEqual(str(doc.contents()), 'only text') + + class TestCallback(TestCase): html = """
    From 478d98a164e3a7bc340db4b150c08e4e508e11bf Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Fri, 17 Jul 2026 23:52:07 -0700 Subject: [PATCH 2/3] refactor: share text-node serialize helper __str__/__unicode__/__html__ used the same contents()-safe join; one helper. --- pyquery/pyquery.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/pyquery/pyquery.py b/pyquery/pyquery.py index f5e7ef1..b7bfb29 100644 --- a/pyquery/pyquery.py +++ b/pyquery/pyquery.py @@ -344,6 +344,15 @@ def remove_namespaces(self): el.tag = el.tag.split('}', 1)[1] return self + def _serialize_nodes(self, dumps): + # Text nodes from contents() cannot go through tostring. + has_text_nodes = any(isinstance(e, str) for e in self) + return ''.join( + e if isinstance(e, str) else dumps( + e, encoding=str, with_tail=not has_text_nodes) + for e in self + ) + def __str__(self): """xml representation of current nodes:: @@ -353,21 +362,11 @@ def __str__(self): """ - has_text_nodes = any(isinstance(e, str) for e in self) - return ''.join( - e if isinstance(e, str) else etree.tostring( - e, encoding=str, with_tail=not has_text_nodes) - for e in self - ) + return self._serialize_nodes(etree.tostring) def __unicode__(self): """xml representation of current nodes""" - has_text_nodes = any(isinstance(e, str) for e in self) - return u''.join( - e if isinstance(e, str) else etree.tostring( - e, encoding=str, with_tail=not has_text_nodes) - for e in self - ) + return self._serialize_nodes(etree.tostring) def __html__(self): """html representation of current nodes:: @@ -378,12 +377,7 @@ def __html__(self): """ - has_text_nodes = any(isinstance(e, str) for e in self) - return u''.join( - e if isinstance(e, str) else lxml.html.tostring( - e, encoding=str, with_tail=not has_text_nodes) - for e in self - ) + return self._serialize_nodes(lxml.html.tostring) def __repr__(self): r = [] From 638b0616ddaa4cc331a2d98447b6a8be3b1c1f7f Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:45:15 -0700 Subject: [PATCH 3/3] fix: pass method=xml for str serialization --- pyquery/pyquery.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyquery/pyquery.py b/pyquery/pyquery.py index b7bfb29..be7b4e3 100644 --- a/pyquery/pyquery.py +++ b/pyquery/pyquery.py @@ -344,12 +344,12 @@ def remove_namespaces(self): el.tag = el.tag.split('}', 1)[1] return self - def _serialize_nodes(self, dumps): + def _serialize_nodes(self, dumps, method): # Text nodes from contents() cannot go through tostring. has_text_nodes = any(isinstance(e, str) for e in self) return ''.join( e if isinstance(e, str) else dumps( - e, encoding=str, with_tail=not has_text_nodes) + e, encoding=str, method=method, with_tail=not has_text_nodes) for e in self ) @@ -362,11 +362,11 @@ def __str__(self): """ - return self._serialize_nodes(etree.tostring) + return self._serialize_nodes(etree.tostring, method='xml') def __unicode__(self): """xml representation of current nodes""" - return self._serialize_nodes(etree.tostring) + return self._serialize_nodes(etree.tostring, method='xml') def __html__(self): """html representation of current nodes:: @@ -377,7 +377,7 @@ def __html__(self): """ - return self._serialize_nodes(lxml.html.tostring) + return self._serialize_nodes(lxml.html.tostring, method='html') def __repr__(self): r = []