Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-pyquery for openSUSE:Factory 
checked in at 2026-08-25 13:18:11
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pyquery (Old)
 and      /work/SRC/openSUSE:Factory/.python-pyquery.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pyquery"

Tue Aug 25 13:18:11 2026 rev:28 rq:1373132 version:2.1.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pyquery/python-pyquery.changes    
2025-09-25 18:44:15.651573281 +0200
+++ /work/SRC/openSUSE:Factory/.python-pyquery.new.1258/python-pyquery.changes  
2026-08-25 13:18:17.824167979 +0200
@@ -1,0 +2,10 @@
+Sat Aug 22 19:00:58 UTC 2026 - Matej Cepl <[email protected]>
+
+- Update to 2.1.0:
+  * Drop support for Python 3.9 and 3.10, add support for Python 3.13.
+  * Drop unicode_literals and modernize formatting with f-strings.
+  * Empty/whitespace-only markup now returns empty document matching jQuery.
+  * Handle option value fallback to option text.
+- Refresh make_webtest_optional.patch
+
+-------------------------------------------------------------------

Old:
----
  pyquery-2.0.1.tar.gz

New:
----
  pyquery-2.1.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pyquery.spec ++++++
--- /var/tmp/diff_new_pack.IaXotM/_old  2026-08-25 13:18:18.575194438 +0200
+++ /var/tmp/diff_new_pack.IaXotM/_new  2026-08-25 13:18:18.576194474 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-pyquery
 #
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -27,7 +27,7 @@
 %endif
 %{?sle15_python_module_pythons}
 Name:           python-pyquery%{psuffix}
-Version:        2.0.1
+Version:        2.1.0
 Release:        0
 Summary:        A jQuery-like library for python
 License:        BSD-3-Clause
@@ -36,14 +36,14 @@
 # PATCH-FIX-OPENSUSE make_webtest_optional.patch [email protected]
 # Make it possible to run test suite (albeit partial) without WebTest module
 Patch0:         make_webtest_optional.patch
-BuildRequires:  %{python_module cssselect >= 1.2.0}
+BuildRequires:  %{python_module cssselect >= 1.5.0}
 BuildRequires:  %{python_module lxml >= 2.1}
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module setuptools}
 BuildRequires:  %{python_module wheel}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
-Requires:       python-cssselect >= 1.2.0
+Requires:       python-cssselect >= 1.5.0
 Requires:       python-lxml >= 2.1
 BuildArch:      noarch
 %if %{with test}

++++++ make_webtest_optional.patch ++++++
--- /var/tmp/diff_new_pack.IaXotM/_old  2026-08-25 13:18:18.611195707 +0200
+++ /var/tmp/diff_new_pack.IaXotM/_new  2026-08-25 13:18:18.617195918 +0200
@@ -1,8 +1,10 @@
-diff -Nru pyquery-2.0.1.orig/conftest.py pyquery-2.0.1/conftest.py
---- pyquery-2.0.1.orig/conftest.py     2024-08-30 10:12:19.000000000 +0200
-+++ pyquery-2.0.1/conftest.py  2024-11-15 14:36:47.617602776 +0100
-@@ -1,7 +1,12 @@
- import os
+Index: pyquery-2.1.0/conftest.py
+===================================================================
+--- pyquery-2.1.0.orig/conftest.py     2026-07-27 21:00:02.000000000 +0200
++++ pyquery-2.1.0/conftest.py  2026-08-22 20:58:58.803206680 +0200
+@@ -2,8 +2,12 @@
+ from urllib.request import urlopen
+ 
  import pytest
 -from webtest import http
 -from webtest.debugapp import debug_app
@@ -12,30 +14,34 @@
 +    from webtest.debugapp import debug_app
 +except (ImportError, ModuleNotFoundError):
 +    http = None
-+
- from urllib.request import urlopen
  
  
-diff -Nru pyquery-2.0.1.orig/tests/test_pyquery.py 
pyquery-2.0.1/tests/test_pyquery.py
---- pyquery-2.0.1.orig/tests/test_pyquery.py   2024-08-30 10:12:19.000000000 
+0200
-+++ pyquery-2.0.1/tests/test_pyquery.py        2024-11-15 14:36:47.607602471 
+0100
-@@ -7,9 +7,12 @@
+ @pytest.fixture
+Index: pyquery-2.1.0/tests/test_pyquery.py
+===================================================================
+--- pyquery-2.1.0.orig/tests/test_pyquery.py   2026-07-27 21:00:02.000000000 
+0200
++++ pyquery-2.1.0/tests/test_pyquery.py        2026-08-22 20:59:04.549266706 
+0200
+@@ -4,12 +4,16 @@
+ import os
+ import sys
+ import time
+-from unittest import TestCase
++from unittest import TestCase, skipIf
+ 
+ import pytest
  from lxml import etree
- from pyquery.pyquery import PyQuery as pq, no_default
- from pyquery.openers import HAS_REQUEST
 -from webtest import http
 -from webtest.debugapp import debug_app
--from unittest import TestCase
-+from unittest import TestCase, skipIf
++
 +try:
 +    from webtest import http
 +    from webtest.debugapp import debug_app
 +except (ImportError, ModuleNotFoundError):
 +    http = None
  
- sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
- 
-@@ -888,6 +891,7 @@
+ from pyquery.openers import HAS_REQUEST
+ from pyquery.pyquery import PyQuery as pq
+@@ -943,6 +947,7 @@
          self.assertEqual(repr(val), repr('b'))
  
  
@@ -43,7 +49,7 @@
  class TestWebScrapping(TestCase):
  
      def setUp(self):
-@@ -932,6 +936,7 @@
+@@ -988,6 +993,7 @@
          self.assertEqual(d('#pt-login').text(), 'Войти')
  
  

++++++ pyquery-2.0.1.tar.gz -> pyquery-2.1.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/CHANGES.rst 
new/pyquery-2.1.0/CHANGES.rst
--- old/pyquery-2.0.1/CHANGES.rst       2024-08-30 10:12:19.000000000 +0200
+++ new/pyquery-2.1.0/CHANGES.rst       2026-07-27 21:00:02.000000000 +0200
@@ -1,3 +1,17 @@
+2.1.0 (2026-07-27)
+------------------
+
+- pseudo selector ``:has()`` no longer support strings as arguments
+
+
+2.0.2 (2026-07-27)
+------------------
+
+- require cssselect 1.4.x. 1.5.x breaks ``has()`` pseudo selector
+
+- Drop support for python 3.9and 3.10
+
+
 2.0.1 (2024-08-30)
 ------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/PKG-INFO new/pyquery-2.1.0/PKG-INFO
--- old/pyquery-2.0.1/PKG-INFO  2024-08-30 10:12:20.397670300 +0200
+++ new/pyquery-2.1.0/PKG-INFO  2026-07-27 21:00:03.079367000 +0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: pyquery
-Version: 2.0.1
+Version: 2.1.0
 Summary: A jquery-like library for python
 Home-page: https://github.com/gawel/pyquery
 Author: Olivier Lauzanne
@@ -12,19 +12,31 @@
 Classifier: Intended Audience :: Developers
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
 License-File: LICENSE.txt
 Requires-Dist: lxml>=2.1
-Requires-Dist: cssselect>=1.2.0
+Requires-Dist: cssselect>=1.5.0
 Provides-Extra: test
 Requires-Dist: requests; extra == "test"
 Requires-Dist: webob; extra == "test"
 Requires-Dist: webtest; extra == "test"
 Requires-Dist: pytest; extra == "test"
 Requires-Dist: pytest-cov; extra == "test"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: home-page
+Dynamic: keywords
+Dynamic: license
+Dynamic: license-file
+Dynamic: maintainer
+Dynamic: maintainer-email
+Dynamic: provides-extra
+Dynamic: requires-dist
+Dynamic: summary
 
 
 pyquery: a jquery-like library for python
@@ -100,6 +112,20 @@
 News
 ====
 
+2.1.0 (2026-07-27)
+------------------
+
+- pseudo selector ``:has()`` no longer support strings as arguments
+
+
+2.0.2 (2026-07-27)
+------------------
+
+- require cssselect 1.4.x. 1.5.x breaks ``has()`` pseudo selector
+
+- Drop support for python 3.9and 3.10
+
+
 2.0.1 (2024-08-30)
 ------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/README_fixt.py 
new/pyquery-2.1.0/README_fixt.py
--- old/pyquery-2.0.1/README_fixt.py    2024-08-30 10:12:19.000000000 +0200
+++ new/pyquery-2.1.0/README_fixt.py    2026-07-27 21:00:02.000000000 +0200
@@ -1,4 +1,5 @@
 import os
+
 from webtest import http
 from webtest.debugapp import debug_app
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/conftest.py 
new/pyquery-2.1.0/conftest.py
--- old/pyquery-2.0.1/conftest.py       2024-08-30 10:12:19.000000000 +0200
+++ new/pyquery-2.1.0/conftest.py       2026-07-27 21:00:02.000000000 +0200
@@ -1,8 +1,9 @@
 import os
+from urllib.request import urlopen
+
 import pytest
 from webtest import http
 from webtest.debugapp import debug_app
-from urllib.request import urlopen
 
 
 @pytest.fixture
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/docs/conf.py 
new/pyquery-2.1.0/docs/conf.py
--- old/pyquery-2.0.1/docs/conf.py      2024-08-30 10:12:19.000000000 +0200
+++ new/pyquery-2.1.0/docs/conf.py      2026-07-27 21:00:02.000000000 +0200
@@ -1,3 +1,4 @@
+#!/usr/bin/env python3
 #
 # pyquery documentation build configuration file, created by
 # sphinx-quickstart on Thu Nov  1 21:48:09 2012.
@@ -252,7 +253,7 @@
 pkg_dir = path.abspath(__file__).split('/docs')[0]
 setup = path.join(pkg_dir, 'setup.py')
 if path.isfile(setup):
-    for line_ in open(setup):
+    for line_ in open(setup):  # NOQA
         if line_.startswith("version"):
             version = line_.split('=')[-1]
             version = version.strip()
@@ -285,6 +286,6 @@
                     continue
                 if t == 'function':
                     k += '()'
-                fd.write('\n\n:%s\n' % k)
+                fd.write(f'\n\n:{k}\n')
                 fd.write('==================\n\n')
-                fd.write(doc.strip('..').replace('        ', '    '))
+                fd.write(doc.strip('..').replace('        ', '    '))  # NOQA
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/docs/conftest.py 
new/pyquery-2.1.0/docs/conftest.py
--- old/pyquery-2.0.1/docs/conftest.py  2024-08-30 10:12:19.000000000 +0200
+++ new/pyquery-2.1.0/docs/conftest.py  2026-07-27 21:00:02.000000000 +0200
@@ -1,5 +1,6 @@
 import os
 import sys
+
 import pytest
 from webtest import http
 from webtest.debugapp import debug_app
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/docs/pseudo_classes.rst 
new/pyquery-2.1.0/docs/pseudo_classes.rst
--- old/pyquery-2.0.1/docs/pseudo_classes.rst   2024-08-30 10:12:19.000000000 
+0200
+++ new/pyquery-2.1.0/docs/pseudo_classes.rst   2026-07-27 21:00:02.000000000 
+0200
@@ -14,7 +14,7 @@
         >>> d(':button')
         [<input>, <button>]
 
-    
+
 
 :checkbox
 ==================
@@ -26,7 +26,7 @@
         >>> d('input:checkbox')
         [<input>]
 
-    
+
 
 :checked
 ==================
@@ -38,7 +38,7 @@
         >>> d('input:checked')
         [<input>]
 
-    
+
 
 :child
 ==================
@@ -55,7 +55,7 @@
         >>> d('h1:contains("title")')
         [<h1.title>]
 
-    
+
 
 :descendant
 ==================
@@ -72,7 +72,7 @@
         >>> d('input:disabled')
         [<input>]
 
-    
+
 
 :empty
 ==================
@@ -84,7 +84,7 @@
         >>> d(':empty')
         [<h2>]
 
-    
+
 
 :enabled
 ==================
@@ -96,7 +96,7 @@
         >>> d('input:enabled')
         [<input>]
 
-    
+
 
 :eq()
 ==================
@@ -110,7 +110,7 @@
         >>> d('h1:eq(1)')
         [<h1.last>]
 
-    
+
 
 :even
 ==================
@@ -122,7 +122,7 @@
         >>> d('p:even')
         [<p>]
 
-    
+
 
 :file
 ==================
@@ -134,7 +134,7 @@
         >>> d('input:file')
         [<input>]
 
-    
+
 
 :first
 ==================
@@ -146,7 +146,7 @@
         >>> d('p:first')
         [<p.first>]
 
-    
+
 
 :gt()
 ==================
@@ -158,7 +158,7 @@
         >>> d('h1:gt(0)')
         [<h1.last>]
 
-    
+
 
 :has()
 ==================
@@ -168,16 +168,16 @@
 
         >>> from pyquery import PyQuery
         >>> d = PyQuery('<div class="foo"><div class="bar"></div></div>')
-        >>> d('.foo:has(".baz")')
+        >>> d('.foo:has(.baz)')
         []
-        >>> d('.foo:has(".foo")')
+        >>> d('.foo:has(.foo)')
         []
-        >>> d('.foo:has(".bar")')
+        >>> d('.foo:has(.bar)')
         [<div.foo>]
         >>> d('.foo:has(div)')
         [<div.foo>]
 
-    
+
 
 :header
 ==================
@@ -189,7 +189,7 @@
         >>> d(':header')
         [<h1>]
 
-    
+
 
 :hidden
 ==================
@@ -201,7 +201,7 @@
         >>> d('input:hidden')
         [<input>]
 
-    
+
 
 :image
 ==================
@@ -213,7 +213,7 @@
         >>> d('input:image')
         [<input>]
 
-    
+
 
 :input
 ==================
@@ -226,7 +226,7 @@
         >>> d(':input')
         [<input>, <textarea>]
 
-    
+
 
 :last
 ==================
@@ -238,7 +238,7 @@
         >>> d('p:last')
         [<p.last>]
 
-    
+
 
 :lt()
 ==================
@@ -250,7 +250,7 @@
         >>> d('h1:lt(1)')
         [<h1.first>]
 
-    
+
 
 :odd
 ==================
@@ -262,7 +262,7 @@
         >>> d('p:odd')
         [<p.last>]
 
-    
+
 
 :parent
 ==================
@@ -274,7 +274,7 @@
         >>> d('h1:parent')
         [<h1>]
 
-    
+
 
 :password
 ==================
@@ -286,7 +286,7 @@
         >>> d('input:password')
         [<input>]
 
-    
+
 
 :pseudo
 ==================
@@ -306,7 +306,7 @@
         >>> d('input:radio')
         [<input>]
 
-    
+
 
 :reset
 ==================
@@ -318,7 +318,7 @@
         >>> d('input:reset')
         [<input>]
 
-    
+
 
 :selected
 ==================
@@ -330,7 +330,7 @@
         >>> d('option:selected')
         [<option>]
 
-    
+
 
 :submit
 ==================
@@ -342,7 +342,7 @@
         >>> d('input:submit')
         [<input>]
 
-    
+
 
 :text
 ==================
@@ -354,4 +354,4 @@
         >>> d('input:text')
         [<input>]
 
-    
\ No newline at end of file
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/pyquery/cssselectpatch.py 
new/pyquery-2.1.0/pyquery/cssselectpatch.py
--- old/pyquery-2.0.1/pyquery/cssselectpatch.py 2024-08-30 10:12:19.000000000 
+0200
+++ new/pyquery-2.1.0/pyquery/cssselectpatch.py 2026-07-27 21:00:02.000000000 
+0200
@@ -1,7 +1,6 @@
 # Copyright (C) 2008 - Olivier Lauzanne <[email protected]>
 #
 # Distributed under the BSD license, see LICENSE.txt
-from __future__ import unicode_literals
 from cssselect import xpath as cssselect_xpath
 from cssselect.xpath import ExpressionError
 
@@ -18,22 +17,18 @@
 
     def add_post_condition(self, post_condition):
         if self.post_condition:
-            self.post_condition = '%s and (%s)' % (self.post_condition,
-                                                   post_condition)
+            self.post_condition = f'{self.post_condition} and 
({post_condition})'
         else:
             self.post_condition = post_condition
 
     def __str__(self):
         path = XPathExprOrig.__str__(self)
         if self.post_condition:
-            path = '%s[%s]' % (path, self.post_condition)
+            path = f'{path}[{self.post_condition}]'
         return path
 
-    def join(self, combiner, other,
-             closing_combiner=None, has_inner_condition=False):
-        res = XPathExprOrig.join(self, combiner, other,
-                                 closing_combiner=closing_combiner,
-                                 has_inner_condition=has_inner_condition)
+    def join(self, combiner, other):
+        res = XPathExprOrig.join(self, combiner, other)
         self.post_condition = other.post_condition
         return res
 
@@ -134,19 +129,19 @@
         /multipage/semantics-other.html#concept-element-disabled
         """
         bool_op = '' if disabled else 'not'
-        return '''(
+        return f'''(
             ((name(.) = 'button' or name(.) = 'input' or name(.) = 'select'
                     or name(.) = 'textarea' or name(.) = 'fieldset')
-                and %s(@disabled or (ancestor::fieldset[@disabled]
+                and {bool_op}(@disabled or (ancestor::fieldset[@disabled]
                     and not(ancestor::legend[not(preceding-sibling::legend)])))
             )
             or
             ((name(.) = 'option'
-                and %s(@disabled or ancestor::optgroup[@disabled]))
+                and {bool_op}(@disabled or ancestor::optgroup[@disabled]))
             )
             or
-            ((name(.) = 'optgroup' and %s(@disabled)))
-            )''' % (bool_op, bool_op, bool_op)
+            ((name(.) = 'optgroup' and {bool_op}(@disabled)))
+            )'''
 
     def xpath_disabled_pseudo(self, xpath):
         """Matches all elements that are disabled::
@@ -198,9 +193,9 @@
 
         ..
         """
-        xpath.add_condition((
+        xpath.add_condition(
             "(name(.) = 'input' or name(.) = 'select') "
-            "or (name(.) = 'textarea' or name(.) = 'button')"))
+            "or (name(.) = 'textarea' or name(.) = 'button')")
         return xpath
 
     def xpath_button_pseudo(self, xpath):
@@ -214,9 +209,9 @@
 
         ..
         """
-        xpath.add_condition((
+        xpath.add_condition(
             "(@type = 'button' and name(.) = 'input') "
-            "or name(.) = 'button'"))
+            "or name(.) = 'button'")
         return xpath
 
     def xpath_radio_pseudo(self, xpath):
@@ -334,9 +329,9 @@
         ..
         """
         # this seems kind of brute-force, is there a better way?
-        xpath.add_condition((
+        xpath.add_condition(
             "(name(.) = 'h1' or name(.) = 'h2' or name (.) = 'h3') "
-            "or (name(.) = 'h4' or name (.) = 'h5' or name(.) = 'h6')"))
+            "or (name(.) = 'h4' or name (.) = 'h5' or name(.) = 'h6')")
         return xpath
 
     def xpath_parent_pseudo(self, xpath):
@@ -379,8 +374,7 @@
         """
         if function.argument_types() != ['NUMBER']:
             raise ExpressionError(
-                "Expected a single integer for :eq(), got %r" % (
-                    function.arguments,))
+                f"Expected a single integer for :eq(), got 
{function.arguments!r}")
         value = int(function.arguments[0].value)
         xpath.add_post_condition('position() = %s' % (value + 1))
         return xpath
@@ -397,8 +391,7 @@
         """
         if function.argument_types() != ['NUMBER']:
             raise ExpressionError(
-                "Expected a single integer for :gt(), got %r" % (
-                    function.arguments,))
+                f"Expected a single integer for :gt(), got 
{function.arguments!r}")
         value = int(function.arguments[0].value)
         xpath.add_post_condition('position() > %s' % (value + 1))
         return xpath
@@ -415,8 +408,7 @@
         """
         if function.argument_types() != ['NUMBER']:
             raise ExpressionError(
-                "Expected a single integer for :gt(), got %r" % (
-                    function.arguments,))
+                f"Expected a single integer for :gt(), got 
{function.arguments!r}")
 
         value = int(function.arguments[0].value)
         xpath.add_post_condition('position() < %s' % (value + 1))
@@ -434,11 +426,10 @@
         """
         if function.argument_types() not in (['STRING'], ['IDENT']):
             raise ExpressionError(
-                "Expected a single string or ident for :contains(), got %r" % (
-                    function.arguments,))
+                f"Expected a single string or ident for :contains(), got 
{function.arguments!r}")
 
         value = self.xpath_literal(function.arguments[0].value)
-        xpath.add_post_condition('contains(., %s)' % value)
+        xpath.add_post_condition(f'contains(., {value})')
         return xpath
 
     def xpath_has_function(self, xpath, function):
@@ -447,11 +438,11 @@
 
             >>> from pyquery import PyQuery
             >>> d = PyQuery('<div class="foo"><div class="bar"></div></div>')
-            >>> d('.foo:has(".baz")')
+            >>> d('.foo:has(.baz)')
             []
-            >>> d('.foo:has(".foo")')
+            >>> d('.foo:has(.foo)')
             []
-            >>> d('.foo:has(".bar")')
+            >>> d('.foo:has(.bar)')
             [<div.foo>]
             >>> d('.foo:has(div)')
             [<div.foo>]
@@ -460,8 +451,7 @@
         """
         if function.argument_types() not in (['STRING'], ['IDENT']):
             raise ExpressionError(
-                "Expected a single string or ident for :has(), got %r" % (
-                    function.arguments,))
+                f"Expected a single string or ident for :has(), got 
{function.arguments!r}")
         value = self.css_to_xpath(
             function.arguments[0].value, prefix='descendant::',
         )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/pyquery/openers.py 
new/pyquery-2.1.0/pyquery/openers.py
--- old/pyquery-2.0.1/pyquery/openers.py        2024-08-30 10:12:19.000000000 
+0200
+++ new/pyquery-2.1.0/pyquery/openers.py        2026-07-27 21:00:02.000000000 
+0200
@@ -1,6 +1,6 @@
-from urllib.request import urlopen
-from urllib.parse import urlencode
 from urllib.error import HTTPError
+from urllib.parse import urlencode
+from urllib.request import urlopen
 
 try:
     import requests
@@ -49,7 +49,7 @@
     else:
         meth = getattr(requests, str(method))
     if method == 'get':
-        url, data = _query(url, method, kwargs)
+        url, _data = _query(url, method, kwargs)
     kw = {}
     for k in allowed_args:
         if k in kwargs:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/pyquery/pyquery.py 
new/pyquery-2.1.0/pyquery/pyquery.py
--- old/pyquery-2.0.1/pyquery/pyquery.py        2024-08-30 10:12:19.000000000 
+0200
+++ new/pyquery-2.1.0/pyquery/pyquery.py        2026-07-27 21:00:02.000000000 
+0200
@@ -1,20 +1,21 @@
 # Copyright (C) 2008 - Olivier Lauzanne <[email protected]>
 #
 # Distributed under the BSD license, see LICENSE.txt
-from .cssselectpatch import JQueryTranslator
-from reprlib import recursive_repr
-from urllib.parse import urlencode
-from urllib.parse import urljoin
-from .openers import url_opener
-from .text import extract_text
-from copy import deepcopy
-from html import escape
-from lxml import etree
-import lxml.html
 import inspect
 import itertools
-import types
 import sys
+import types
+from copy import deepcopy
+from html import escape
+from reprlib import recursive_repr
+from urllib.parse import urlencode, urljoin
+
+import lxml.html
+from lxml import etree
+
+from .cssselectpatch import JQueryTranslator
+from .openers import url_opener
+from .text import extract_text
 
 if sys.version_info >= (3, 12, 0):
     from collections import OrderedDict
@@ -30,8 +31,8 @@
         def __repr__(self):
             'od.__repr__() <==> repr(od)'
             if not self:
-                return '%s()' % (self.__class__.__name__,)
-            return '%s(%r)' % (self.__class__.__name__, dict(self.items()))
+                return f'{self.__class__.__name__}()'
+            return f'{self.__class__.__name__}({dict(self.items())!r})'
 
 basestring = (str, bytes)
 
@@ -60,14 +61,17 @@
         f = types.FunctionType(func.__code__, func.__globals__,
                                name, func.__defaults__)
         f.__doc__ = (
-            'Alias for :func:`~pyquery.pyquery.PyQuery.%s`') % func.__name__
+            f'Alias for :func:`~pyquery.pyquery.PyQuery.{func.__name__}`')
         setattr(PyQuery, name, f.__get__(None, PyQuery))
 
 
 def fromstring(context, parser=None, custom_parser=None):
     """use html parser if we don't have clean xml
     """
-    if hasattr(context, 'read') and hasattr(context.read, '__call__'):
+    # Empty/whitespace-only markup → empty document (jQuery $('') parity).
+    if isinstance(context, basestring) and not context.strip():
+        return []
+    if hasattr(context, 'read') and callable(context.read):
         meth = 'parse'
     else:
         meth = 'fromstring'
@@ -96,7 +100,7 @@
         elif parser == 'html_fragments':
             custom_parser = lxml.html.fragments_fromstring
         else:
-            raise ValueError('No such parser: "%s"' % parser)
+            raise ValueError(f'No such parser: "{parser}"')
 
     result = custom_parser(context)
     if isinstance(result, list):
@@ -113,7 +117,7 @@
     return func(*args[:func.__code__.co_argcount])
 
 
-class NoDefault(object):
+class NoDefault:
     def __repr__(self):
         """clean representation in Sphinx"""
         return '<NoDefault>'
@@ -123,7 +127,7 @@
 del NoDefault
 
 
-class FlexibleElement(object):
+class FlexibleElement:
     """property to allow a flexible api"""
     def __init__(self, pget, pset=no_default, pdel=no_default):
         self.pget = pget
@@ -131,7 +135,7 @@
         self.pdel = pdel
 
     def __get__(self, instance, klass):
-        class _element(object):
+        class _element:
             """real element to support set/get/del attr and item and js call
             style"""
             def __call__(prop, *args, **kwargs):
@@ -146,7 +150,7 @@
             __delattr__ = __delitem__
 
             def __repr__(prop):
-                return '<flexible_element %s>' % self.pget.__name__
+                return f'<flexible_element {self.pget.__name__}>'
         return _element()
 
     def __set__(self, instance, value):
@@ -187,7 +191,7 @@
         if kwargs:
             # specific case to get the dom
             if 'filename' in kwargs:
-                html = open(kwargs['filename'],
+                html = open(kwargs['filename'],  # NOQA
                             encoding=kwargs.get('encoding'))
             elif 'url' in kwargs:
                 url = kwargs.pop('url')
@@ -200,14 +204,14 @@
                     self.parser = 'html'
                 self._base_url = url
             else:
-                raise ValueError('Invalid keyword arguments %s' % kwargs)
+                raise ValueError(f'Invalid keyword arguments {kwargs}')
 
             elements = fromstring(html, self.parser)
             # close open descriptor if possible
             if hasattr(html, 'close'):
                 try:
                     html.close()
-                except Exception:
+                except Exception:  # NOQA
                     pass
 
         else:
@@ -228,7 +232,7 @@
             if isinstance(context, basestring):
                 try:
                     elements = fromstring(context, self.parser)
-                except Exception:
+                except Exception:  # NOQA
                     raise
             elif isinstance(context, self.__class__):
                 # copy
@@ -344,6 +348,15 @@
                     el.tag = el.tag.split('}', 1)[1]
         return self
 
+    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, method=method, with_tail=not has_text_nodes)
+            for e in self
+        )
+
     def __str__(self):
         """xml representation of current nodes::
 
@@ -353,12 +366,11 @@
             <script>&lt;![[CDATA[ ]&gt;</script>
 
         """
-        return ''.join([etree.tostring(e, encoding=str) for e in self])
+        return self._serialize_nodes(etree.tostring, method='xml')
 
     def __unicode__(self):
         """xml representation of current nodes"""
-        return u''.join([etree.tostring(e, encoding=str)
-                         for e in self])
+        return self._serialize_nodes(etree.tostring, method='xml')
 
     def __html__(self):
         """html representation of current nodes::
@@ -369,8 +381,7 @@
             <script><![[CDATA[ ]></script>
 
         """
-        return u''.join([lxml.html.tostring(e, encoding=str)
-                         for e in self])
+        return self._serialize_nodes(lxml.html.tostring, method='html')
 
     def __repr__(self):
         r = []
@@ -380,7 +391,7 @@
                 c = c and '.' + '.'.join(c.split(' ')) or ''
                 id = el.get('id')
                 id = id and '#' + id or ''
-                r.append('<%s%s%s>' % (el.tag, id, c))
+                r.append(f'<{el.tag}{id}{c}>')
             return '[' + (', '.join(r)) + ']'
         except AttributeError:
             return list.__repr__(self)
@@ -457,8 +468,7 @@
                 this_list.append(current)
                 current = current.getparent()
             this_list.reverse()
-            for j in this_list:
-                yield j
+            yield from this_list
 
     def _next_all(self):
         return [e for e in self._traverse('getnext')]
@@ -604,7 +614,7 @@
             >>> d('p').filter(lambda i, this: PyQuery(this).text() == 'Hi')
             [<p.hello>]
         """
-        if not hasattr(selector, '__call__'):
+        if not callable(selector):
             return self._filter_only(selector, self)
         else:
             elements = []
@@ -768,7 +778,8 @@
         length = len(args)
         if length == 1:
             attr = args[0]
-            attr = mapping.get(attr, attr)
+            if not isinstance(attr, dict):
+                attr = mapping.get(attr, attr)
         elif length == 2:
             attr, value = args
             attr = mapping.get(attr, attr)
@@ -777,14 +788,14 @@
             for k, v in kwargs.items():
                 attr[mapping.get(k, k)] = v
         else:
-            raise ValueError('Invalid arguments %s %s' % (args, kwargs))
+            raise ValueError(f'Invalid arguments {args} {kwargs}')
 
         if not self:
             return None
         elif isinstance(attr, dict):
             for tag in self:
                 for key, value in attr.items():
-                    tag.set(key, value)
+                    tag.set(mapping.get(key, key), value)
         elif value is no_default:
             return self[0].get(attr)
         elif value is None:
@@ -840,7 +851,7 @@
 
         ..
         """
-        return self.is_('.%s' % name)
+        return self.is_(f'.{name}')
 
     @with_camel_case_alias
     def add_class(self, value):
@@ -879,9 +890,7 @@
             classes.difference_update(values)
             classes.difference_update([''])
             classes = ' '.join(classes)
-            if classes.strip():
-                tag.set('class', classes)
-            elif tag.get('class'):
+            if classes.strip() or tag.get('class'):
                 tag.set('class', classes)
         return self
 
@@ -919,19 +928,19 @@
         elif kwargs:
             attr = kwargs
         else:
-            raise ValueError('Invalid arguments %s %s' % (args, kwargs))
+            raise ValueError(f'Invalid arguments {args} {kwargs}')
 
         if isinstance(attr, dict):
             for tag in self:
                 stripped_keys = [key.strip().replace('_', '-')
-                                 for key in attr.keys()]
+                                 for key in attr]
                 current = [el.strip()
                            for el in (tag.get('style') or '').split(';')
                            if el.strip()
                            and el.split(':')[0].strip() not in stripped_keys]
                 for key, value in attr.items():
                     key = key.replace('_', '-')
-                    current.append('%s: %s' % (key, value))
+                    current.append(f'{key}: {value}')
                 tag.set('style', '; '.join(current))
         elif isinstance(value, basestring):
             attr = attr.replace('_', '-')
@@ -940,8 +949,8 @@
                     el.strip()
                     for el in (tag.get('style') or '').split(';')
                     if (el.strip() and
-                        not el.split(':')[0].strip() == attr.strip())]
-                current.append('%s: %s' % (attr, value))
+                        el.split(':')[0].strip() != attr.strip())]
+                current.append(f'{attr}: {value}')
                 tag.set('style', '; '.join(current))
         return self
 
@@ -1001,10 +1010,15 @@
             ['you', 'hou']
 
         """
+        def _option_value(option):
+            # Missing value attr uses option text (HTML / jQuery).
+            val = option.attr('value')
+            return option.text() if val is None else val
+
         def _get_value(tag):
             # <textarea>
             if tag.tag == 'textarea':
-                return self._copy(tag).html()
+                return self._copy(tag).html(escape=False)
             # <select>
             elif tag.tag == 'select':
                 if 'multiple' in tag.attrib:
@@ -1012,13 +1026,15 @@
                     selected = self._copy(tag)('option[selected]')
                     # Rebuild list to avoid serialization error
                     return list(selected.map(
-                        lambda _, o: self._copy(o).attr('value')
+                        lambda _, o: _option_value(self._copy(o))
                     ))
                 selected_option = self._copy(tag)('option[selected]:last')
                 if selected_option:
-                    return selected_option.attr('value')
-                else:
-                    return self._copy(tag)('option').attr('value')
+                    return _option_value(selected_option)
+                first = self._copy(tag)('option:first')
+                if first:
+                    return _option_value(first)
+                return None
             # <input type="checkbox"> or <input type="radio">
             elif self.is_(':checkbox,:radio'):
                 val = self._copy(tag).attr('value')
@@ -1042,10 +1058,11 @@
 
                     def _make_option_selected(_, elem):
                         pq = self._copy(elem)
-                        if pq.attr('value') in value:
+                        if pq.attr('value') in value:  # NOQA
                             pq.attr('selected', 'selected')
-                            if 'multiple' not in tag.attrib:
-                                del value[:]  # Ensure it toggles first match
+                            if 'multiple' not in tag.attrib:  # NOQA
+                                # Ensure it toggles first match
+                                del value[:]  # NOQA
                         else:
                             pq.removeAttr('selected')
 
@@ -1097,12 +1114,14 @@
                 return None
             tag = self[0]
             children = tag.getchildren()
-            html = escape(tag.text or '', quote=False)
+            html = tag.text or ''
+            if kwargs.pop('escape', True):
+                html = escape(html, quote=False)
             if not children:
                 return html
             if 'encoding' not in kwargs:
                 kwargs['encoding'] = str
-            html += u''.join([etree.tostring(e, **kwargs)
+            html += ''.join([etree.tostring(e, **kwargs)
                               for e in children])
             return html
         else:
@@ -1119,7 +1138,7 @@
                 for child in tag.getchildren():
                     tag.remove(child)
                 root = fromstring(
-                    u'<root>' + new_html + u'</root>',
+                    '<root>' + new_html + '</root>',
                     self.parser)[0]
                 children = root.getchildren()
                 if children:
@@ -1188,7 +1207,7 @@
             if not self:
                 return ''
             return ' '.join(
-                self._copy(tag).html() if tag.tag == 'textarea' else
+                self._copy(tag).html(escape=False) if tag.tag == 'textarea' 
else
                 extract_text(tag, **kwargs) for tag in self
             )
 
@@ -1204,7 +1223,7 @@
 
     def _get_root(self, value):
         if isinstance(value, basestring):
-            root = fromstring(u'<root>' + value + u'</root>',
+            root = fromstring('<root>' + value + '</root>',
                               self.parser)[0]
         elif isinstance(value, etree._Element):
             root = self._copy(value)
@@ -1212,7 +1231,7 @@
             root = value
         else:
             raise TypeError(
-                'Value must be string, PyQuery or Element. Got %r' % value)
+                f'Value must be string, PyQuery or Element. Got {value!r}')
         if hasattr(root, 'text') and isinstance(root.text, basestring):
             root_text = root.text
         else:
@@ -1422,7 +1441,7 @@
         """
         if isinstance(value, PyQuery):
             value = str(value)
-        if hasattr(value, '__call__'):
+        if callable(value):
             for i, element in enumerate(self):
                 self._copy(element).before(
                     value(i, element) + (element.tail or ''))
@@ -1490,7 +1509,7 @@
             results.remove()
         return self
 
-    class Fn(object):
+    class Fn:
         """Hook for defining custom function (like the jQuery.fn):
 
         .. sourcecode:: python
@@ -1527,10 +1546,7 @@
             >>> d.serializeArray() == [{'name': 'order', 'value': 'spam'}]
             True
         """
-        return list(map(
-            lambda p: {'name': p[0], 'value': p[1]},
-            self.serialize_pairs()
-        ))
+        return [{'name': p[0], 'value': p[1]} for p in self.serialize_pairs()]
 
     def serialize(self):
         """Serialize form elements as a URL-encoded string.
@@ -1573,8 +1589,7 @@
                     # Include inputs outside of their form owner
                     root = self._copy(el.root.getroot())
                     controls.extend(root(
-                        '#%s :not([form]):input, [form="%s"]:input'
-                        % (form_id, form_id)))
+                        f'#{form_id} :not([form]):input, 
[form="{form_id}"]:input'))
                 else:
                     controls.extend(el(':not([form]):input'))
             elif el[0].tag == 'fieldset':
@@ -1583,9 +1598,7 @@
                 controls.extend(el)
         # Filter controls
         selector = '[name]:enabled:not(button)'  # Not serializing image button
-        selector += ''.join(map(
-            lambda s: ':not([type="%s"])' % s,
-            _submitter_types))
+        selector += ''.join(f':not([type="{s}"])' for s in _submitter_types)
         controls = controls.filter(selector)
 
         def _filter_out_unchecked(_, el):
@@ -1608,10 +1621,7 @@
         for field in controls:
             val = self._copy(field).val() or ''
             if isinstance(val, list):
-                ret.extend(map(
-                    lambda v: (field.attrib['name'], v.replace('\n', '\r\n')),
-                    val
-                ))
+                ret.extend((field.attrib['name'], v.replace('\n', '\r\n')) for 
v in val)
             else:
                 ret.append((field.attrib['name'], val.replace('\n', '\r\n')))
         return ret
@@ -1656,9 +1666,9 @@
         if base_url is None:
             base_url = self.base_url
             if base_url is None:
-                raise ValueError((
+                raise ValueError(
                     'You need a base URL to make your links'
-                    'absolute. It can be provided by the base_url parameter.'))
+                    'absolute. It can be provided by the base_url parameter.')
 
         def repl(attr):
             def rep(i, e):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/pyquery/text.py 
new/pyquery-2.1.0/pyquery/text.py
--- old/pyquery-2.0.1/pyquery/text.py   2024-08-30 10:12:19.000000000 +0200
+++ new/pyquery-2.1.0/pyquery/text.py   2026-07-27 21:00:02.000000000 +0200
@@ -1,6 +1,5 @@
 import re
 
-
 # https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements#Elements
 INLINE_TAGS = {
     'a', 'abbr', 'acronym', 'b', 'bdo', 'big', 'br', 'button', 'cite',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/pyquery.egg-info/PKG-INFO 
new/pyquery-2.1.0/pyquery.egg-info/PKG-INFO
--- old/pyquery-2.0.1/pyquery.egg-info/PKG-INFO 2024-08-30 10:12:20.000000000 
+0200
+++ new/pyquery-2.1.0/pyquery.egg-info/PKG-INFO 2026-07-27 21:00:03.000000000 
+0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: pyquery
-Version: 2.0.1
+Version: 2.1.0
 Summary: A jquery-like library for python
 Home-page: https://github.com/gawel/pyquery
 Author: Olivier Lauzanne
@@ -12,19 +12,31 @@
 Classifier: Intended Audience :: Developers
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
 License-File: LICENSE.txt
 Requires-Dist: lxml>=2.1
-Requires-Dist: cssselect>=1.2.0
+Requires-Dist: cssselect>=1.5.0
 Provides-Extra: test
 Requires-Dist: requests; extra == "test"
 Requires-Dist: webob; extra == "test"
 Requires-Dist: webtest; extra == "test"
 Requires-Dist: pytest; extra == "test"
 Requires-Dist: pytest-cov; extra == "test"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: home-page
+Dynamic: keywords
+Dynamic: license
+Dynamic: license-file
+Dynamic: maintainer
+Dynamic: maintainer-email
+Dynamic: provides-extra
+Dynamic: requires-dist
+Dynamic: summary
 
 
 pyquery: a jquery-like library for python
@@ -100,6 +112,20 @@
 News
 ====
 
+2.1.0 (2026-07-27)
+------------------
+
+- pseudo selector ``:has()`` no longer support strings as arguments
+
+
+2.0.2 (2026-07-27)
+------------------
+
+- require cssselect 1.4.x. 1.5.x breaks ``has()`` pseudo selector
+
+- Drop support for python 3.9and 3.10
+
+
 2.0.1 (2024-08-30)
 ------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/pyquery.egg-info/requires.txt 
new/pyquery-2.1.0/pyquery.egg-info/requires.txt
--- old/pyquery-2.0.1/pyquery.egg-info/requires.txt     2024-08-30 
10:12:20.000000000 +0200
+++ new/pyquery-2.1.0/pyquery.egg-info/requires.txt     2026-07-27 
21:00:03.000000000 +0200
@@ -1,5 +1,5 @@
 lxml>=2.1
-cssselect>=1.2.0
+cssselect>=1.5.0
 
 [test]
 requests
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/setup.py new/pyquery-2.1.0/setup.py
--- old/pyquery-2.0.1/setup.py  2024-08-30 10:12:19.000000000 +0200
+++ new/pyquery-2.1.0/setup.py  2026-07-27 21:00:02.000000000 +0200
@@ -3,22 +3,22 @@
 #
 # Distributed under the BSD license, see LICENSE.txt
 
-from setuptools import setup, find_packages
 import os
 
+from setuptools import find_packages, setup
 
 install_requires = [
     'lxml>=2.1',
-    'cssselect>=1.2.0',
+    'cssselect>=1.5.0',
 ]
 
 
 def read(*names):
-    values = dict()
+    values = {}
     for name in names:
         filename = name + '.rst'
         if os.path.isfile(filename):
-            fd = open(filename)
+            fd = open(filename)  # NOQA
             value = fd.read()
             fd.close()
         else:
@@ -28,18 +28,18 @@
 
 
 long_description = """
-%(README)s
+{README}
 
 See http://pyquery.rtfd.org/ for the full documentation
 
 News
 ====
 
-%(CHANGES)s
+{CHANGES}
 
-""" % read('README', 'CHANGES')
+""".format(**read('README', 'CHANGES'))
 
-version = '2.0.1'
+version = '2.1.0'
 
 setup(name='pyquery',
       version=version,
@@ -49,10 +49,9 @@
           "Intended Audience :: Developers",
           "Development Status :: 5 - Production/Stable",
           "Programming Language :: Python :: 3",
-          "Programming Language :: Python :: 3.9",
-          "Programming Language :: Python :: 3.10",
           "Programming Language :: Python :: 3.11",
           "Programming Language :: Python :: 3.12",
+          "Programming Language :: Python :: 3.13",
       ],
       keywords='jquery html xml scraping',
       author='Olivier Lauzanne',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/tests/apps.py 
new/pyquery-2.1.0/tests/apps.py
--- old/pyquery-2.0.1/tests/apps.py     2024-08-30 10:12:19.000000000 +0200
+++ new/pyquery-2.1.0/tests/apps.py     2026-07-27 21:00:02.000000000 +0200
@@ -1,6 +1,4 @@
-from webob import Request
-from webob import Response
-from webob import exc
+from webob import Request, Response, exc
 
 
 def input_app(environ, start_response):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/tests/browser_base.py 
new/pyquery-2.1.0/tests/browser_base.py
--- old/pyquery-2.0.1/tests/browser_base.py     2024-08-30 10:12:19.000000000 
+0200
+++ new/pyquery-2.1.0/tests/browser_base.py     2026-07-27 21:00:02.000000000 
+0200
@@ -1,4 +1,4 @@
-class TextExtractionMixin():
+class TextExtractionMixin:
     def _prepare_dom(self, html):
         self.last_html = '<html><body>' + html + '</body></html>'
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/tests/test_browser.py 
new/pyquery-2.1.0/tests/test_browser.py
--- old/pyquery-2.0.1/tests/test_browser.py     2024-08-30 10:12:19.000000000 
+0200
+++ new/pyquery-2.1.0/tests/test_browser.py     2026-07-27 21:00:02.000000000 
+0200
@@ -1,6 +1,7 @@
 import unittest
 
 from pyquery.pyquery import PyQuery
+
 from .browser_base import TextExtractionMixin
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/tests/test_pyquery.py 
new/pyquery-2.1.0/tests/test_pyquery.py
--- old/pyquery-2.0.1/tests/test_pyquery.py     2024-08-30 10:12:19.000000000 
+0200
+++ new/pyquery-2.1.0/tests/test_pyquery.py     2026-07-27 21:00:02.000000000 
+0200
@@ -4,12 +4,16 @@
 import os
 import sys
 import time
+from unittest import TestCase
+
+import pytest
 from lxml import etree
-from pyquery.pyquery import PyQuery as pq, no_default
-from pyquery.openers import HAS_REQUEST
 from webtest import http
 from webtest.debugapp import debug_app
-from unittest import TestCase
+
+from pyquery.openers import HAS_REQUEST
+from pyquery.pyquery import PyQuery as pq
+from pyquery.pyquery import no_default
 
 sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
 
@@ -349,6 +353,14 @@
     def test_typeerror_on_invalid_value(self):
         self.assertRaises(TypeError, pq, object())
 
+    def test_empty_string_returns_empty_doc(self):
+        """Empty/whitespace markup must not raise ParserError."""
+        for markup in ('', '   ', '\n', '\t  \n'):
+            doc = pq(markup)
+            self.assertEqual(len(doc), 0)
+            self.assertEqual(doc.html(), None)
+            self.assertEqual(list(doc), [])
+
 
 class TestComment(TestCase):
 
@@ -357,6 +369,19 @@
         self.assertEqual(doc.text(), 'bar')
 
 
+class TestContentsStr(TestCase):
+
+    def test_str_contents_with_text_nodes(self):
+        doc = pq('hello <b>bold</b> world')
+        contents = doc.contents()
+        self.assertEqual(str(contents), 'hello <b>bold</b> world')
+        self.assertEqual(contents.__html__(), 'hello <b>bold</b> world')
+
+    def test_str_contents_text_only(self):
+        doc = pq('<div>only text</div>')
+        self.assertEqual(str(doc.contents()), 'only text')
+
+
 class TestCallback(TestCase):
     html = """
         <ol>
@@ -487,6 +512,15 @@
         self.assertEqual(d.outer_html(), '<div value=""></div>')
         self.assertEqual(d.outer_html(method="xml"), '<div value=""/>')
 
+    def test_attr_dict_mapping(self):
+        d = pq('<div>')
+        d.attr({'id': 'x', 'class_': 'y', 'data-a': '1'})
+        self.assertEqual(d.attr('id'), 'x')
+        self.assertEqual(d.attr('class'), 'y')
+        self.assertEqual(d.attr('data-a'), '1')
+        self.assertEqual(d.attr(id='z', class_='w').attr('id'), 'z')
+        self.assertEqual(d.attr('class'), 'w')
+
     def test_remove(self):
         d = pq(self.html)
         d('img').remove()
@@ -534,10 +568,9 @@
         self.assertEqual(d('#textarea-multi').val(), multi_expected)
         self.assertEqual(d('#textarea-multi').text(), multi_expected)
         multi_new = '''Bacon\n<b>Eggs</b>\nSpam'''
-        multi_new_expected = '''Bacon\n&lt;b&gt;Eggs&lt;/b&gt;\nSpam'''
         d('#textarea-multi').val(multi_new)
-        self.assertEqual(d('#textarea-multi').val(), multi_new_expected)
-        self.assertEqual(d('#textarea-multi').text(), multi_new_expected)
+        self.assertEqual(d('#textarea-multi').val(), multi_new)
+        self.assertEqual(d('#textarea-multi').text(), multi_new)
 
     def test_val_for_select(self):
         d = pq(self.html4)
@@ -593,6 +626,27 @@
         self.assertEqual(d('#second').val(), '42')
         self.assertEqual(d('#third').val(), '42')
 
+    def test_val_select_option_without_value_attr(self):
+        d = pq('<select><option>a</option><option 
selected>b</option></select>')
+        self.assertEqual(d.val(), 'b')
+        d = 
pq('<select><option>first</option><option>second</option></select>')
+        self.assertEqual(d.val(), 'first')
+        d = pq('<select><option selected value="">empty</option></select>')
+        self.assertEqual(d.val(), '')
+        d = pq(
+            '<select multiple>'
+            '<option selected>a</option>'
+            '<option selected>b</option>'
+            '</select>'
+        )
+        self.assertEqual(d.val(), ['a', 'b'])
+        form = pq(
+            '<form><select name="s">'
+            '<option>t1</option><option selected>t2</option>'
+            '</select></form>'
+        )
+        self.assertEqual(form.serialize_pairs(), [('s', 't2')])
+
     def test_val_checkbox_no_value_attribute(self):
         d = pq('<input type="checkbox">')
         self.assertEqual(d.val(), 'on')
@@ -784,6 +838,7 @@
                          'http://example.com/path_info')
 
 
[email protected](os.name == 'nt', reason='fail on windows')
 class TestHTMLParser(TestCase):
     xml = "<div>I'm valid XML</div>"
     html = '''<div class="portlet">
@@ -802,7 +857,7 @@
         expected = '''<div class="portlet">
       <a href="/toto">TestimageMy link text</a>
       <a href="/toto2">imageMy link text 2</a>
-      Behind you, a three-headed HTML&amp;dash;Entity!
+      Behind you, a three-headed HTML‐Entity!
     </div>'''
         d = pq(self.html)
         d('img').replace_with('image')
@@ -813,7 +868,7 @@
         expected = '''<div class="portlet">
       TestimageMy link text
       imageMy link text 2
-      Behind you, a three-headed HTML&amp;dash;Entity!
+      Behind you, a three-headed HTML‐Entity!
     </div>'''
         d = pq(self.html)
         d('a').replace_with(lambda i, e: pq(e).html())
@@ -838,7 +893,7 @@
     </body>
     </html>'''
 
-    namespaces = {'bar': 'http://example.com/bar',
+    namespaces = {'bar': 'http://example.com/bar',  # NOQA
                   'baz': 'http://example.com/baz'}
 
     def test_selector(self):
@@ -899,14 +954,14 @@
         d = pq(url=self.application_url, data={'q': 'foo'},
                method='get')
         print(d)
-        self.assertIn('REQUEST_METHOD: GET', d('p').text())
-        self.assertIn('q=foo', d('p').text())
+        self.assertIn('REQUEST_METHOD: GET', d.text())
+        self.assertIn('q=foo', d.text())
 
     def test_post(self):
         d = pq(url=self.application_url, data={'q': 'foo'},
                method='post')
-        self.assertIn('REQUEST_METHOD: POST', d('p').text())
-        self.assertIn('q=foo', d('p').text())
+        self.assertIn('REQUEST_METHOD: POST', d.text())
+        self.assertIn('q=foo', d.text())
 
     def test_session(self):
         if HAS_REQUEST:
@@ -915,7 +970,7 @@
             session.headers.update({'X-FOO': 'bar'})
             d = pq(url=self.application_url, data={'q': 'foo'},
                    method='get', session=session)
-            self.assertIn('HTTP_X_FOO: bar', d('p').text())
+            self.assertIn('HTTP_X_FOO: bar', d.text())
         else:
             self.skipTest('no requests library')
 
@@ -925,6 +980,7 @@
 
 class TestWebScrappingEncoding(TestCase):
 
+    @pytest.mark.skip('No longer possible to query this url')
     def test_get(self):
         d = pq(url='http://ru.wikipedia.org/wiki/Заглавная_страница',
                method='get')
@@ -945,7 +1001,7 @@
 
     def test_get(self):
         pq(url=self.application_url)
-        with self.assertRaises(Exception):
+        with self.assertRaises(Exception):  # NOQA
             pq(url=self.application_url, timeout=1)
 
     def tearDown(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/tests/test_real_browser.py 
new/pyquery-2.1.0/tests/test_real_browser.py
--- old/pyquery-2.0.1/tests/test_real_browser.py        2024-08-30 
10:12:19.000000000 +0200
+++ new/pyquery-2.1.0/tests/test_real_browser.py        2026-07-27 
21:00:02.000000000 +0200
@@ -14,9 +14,9 @@
     SELENIUM = False
 
 if SELENIUM:
-    from urllib.parse import urlunsplit
-    from http.server import HTTPServer, BaseHTTPRequestHandler
+    from http.server import BaseHTTPRequestHandler, HTTPServer
     from queue import Queue
+    from urllib.parse import urlunsplit
 
     class BaseTestRequestHandler(BaseHTTPRequestHandler):
         _last_html = ''
@@ -99,8 +99,7 @@
 
         def open_url(self, path):
             self.driver.get(urlunsplit(
-                ('http', '{}:{}'.format(
-                    self.LOCAL_IP, self.PORT), path, '', '')))
+                ('http', f'{self.LOCAL_IP}:{self.PORT}', path, '', '')))
 
     class TestInnerText(BaseBrowserTest, TextExtractionMixin):
         REQUEST_HANDLER_CLASS = HTMLSnippetSender
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyquery-2.0.1/tox.ini new/pyquery-2.1.0/tox.ini
--- old/pyquery-2.0.1/tox.ini   2024-08-30 10:12:19.000000000 +0200
+++ new/pyquery-2.1.0/tox.ini   2026-07-27 21:00:02.000000000 +0200
@@ -1,5 +1,5 @@
 [tox]
-envlist=py38,py39,py310,py311,py312
+envlist=py311,py312
 
 [testenv]
 whitelist_externals=
@@ -15,7 +15,7 @@
 [testenv:lint]
 skipsdist=true
 skip_install=true
-basepython = python3.11
+basepython = python3.12
 commands =
     ruff check
 deps =
@@ -24,7 +24,7 @@
 [testenv:docs]
 skip_install=false
 skipsdist=true
-basepython = python3.11
+basepython = python3.12
 changedir = docs
 deps =
     sphinx

Reply via email to