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 = []