Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-cssselect for 
openSUSE:Factory checked in at 2026-03-30 18:29:45
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-cssselect (Old)
 and      /work/SRC/openSUSE:Factory/.python-cssselect.new.1999 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-cssselect"

Mon Mar 30 18:29:45 2026 rev:16 rq:1343406 version:1.4.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-cssselect/python-cssselect.changes        
2025-03-25 22:07:38.206806868 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-cssselect.new.1999/python-cssselect.changes  
    2026-03-30 18:29:55.059624862 +0200
@@ -1,0 +2,9 @@
+Sun Mar 29 10:19:05 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 1.4.0:
+  * Dropped support for Python 3.9 and PyPy 3.10.
+  * Added support for Python 3.14 and PyPy 3.11.
+  * Switched the build system to ``hatchling``.
+  * CI fixes and improvements.
+
+-------------------------------------------------------------------

Old:
----
  v1.3.0.tar.gz

New:
----
  v1.4.0.tar.gz

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

Other differences:
------------------
++++++ python-cssselect.spec ++++++
--- /var/tmp/diff_new_pack.L7IDOQ/_old  2026-03-30 18:29:55.763654123 +0200
+++ /var/tmp/diff_new_pack.L7IDOQ/_new  2026-03-30 18:29:55.763654123 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-cssselect
 #
-# Copyright (c) 2025 SUSE LLC
+# 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
@@ -26,15 +26,15 @@
 %endif
 %{?sle15_python_module_pythons}
 Name:           python-cssselect%{psuffix}
-Version:        1.3.0
+Version:        1.4.0
 Release:        0
 Summary:        CSS3 selectors for Python
 License:        BSD-3-Clause
 Group:          Development/Languages/Python
 URL:            https://github.com/scrapy/cssselect
 Source:         https://github.com/scrapy/cssselect/archive/v%{version}.tar.gz
+BuildRequires:  %{python_module hatch_vcs}
 BuildRequires:  %{python_module pip}
-BuildRequires:  %{python_module setuptools}
 BuildRequires:  %{python_module wheel}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros

++++++ v1.3.0.tar.gz -> v1.4.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/.git-blame-ignore-revs 
new/cssselect-1.4.0/.git-blame-ignore-revs
--- old/cssselect-1.3.0/.git-blame-ignore-revs  2025-03-10 10:20:12.000000000 
+0100
+++ new/cssselect-1.4.0/.git-blame-ignore-revs  2026-01-29 07:59:11.000000000 
+0100
@@ -1,2 +1,2 @@
 # applying pre-commit hooks to the project
-e91101b37f82558db84a6b8ee9a6dba1fd2ae0bb
\ No newline at end of file
+e91101b37f82558db84a6b8ee9a6dba1fd2ae0bb
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/.github/workflows/checks.yml 
new/cssselect-1.4.0/.github/workflows/checks.yml
--- old/cssselect-1.3.0/.github/workflows/checks.yml    2025-03-10 
10:20:12.000000000 +0100
+++ new/cssselect-1.4.0/.github/workflows/checks.yml    2026-01-29 
07:59:11.000000000 +0100
@@ -8,24 +8,24 @@
       fail-fast: false
       matrix:
         include:
-        - python-version: 3.13
+        - python-version: 3.14
           env:
             TOXENV: pylint
-        - python-version: 3.13  # Keep in sync with .readthedocs.yml
+        - python-version: 3.14  # Keep in sync with .readthedocs.yml
           env:
             TOXENV: docs
-        - python-version: 3.13
+        - python-version: 3.14
           env:
             TOXENV: typing
-        - python-version: 3.13
+        - python-version: 3.14
           env:
             TOXENV: twinecheck
 
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v6
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
         python-version: ${{ matrix.python-version }}
 
@@ -35,9 +35,9 @@
         pip install -U pip
         pip install -U tox
         tox
-  
+
   pre-commit:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v6
     - uses: pre-commit/[email protected]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/.github/workflows/publish.yml 
new/cssselect-1.4.0/.github/workflows/publish.yml
--- old/cssselect-1.3.0/.github/workflows/publish.yml   2025-03-10 
10:20:12.000000000 +0100
+++ new/cssselect-1.4.0/.github/workflows/publish.yml   2026-01-29 
07:59:11.000000000 +0100
@@ -16,12 +16,12 @@
       id-token: write
 
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v6
 
     - name: Set up Python
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
-        python-version: 3.13
+        python-version: 3.14
 
     - name: Build
       run: |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/.github/workflows/tests-macos.yml 
new/cssselect-1.4.0/.github/workflows/tests-macos.yml
--- old/cssselect-1.3.0/.github/workflows/tests-macos.yml       1970-01-01 
01:00:00.000000000 +0100
+++ new/cssselect-1.4.0/.github/workflows/tests-macos.yml       2026-01-29 
07:59:11.000000000 +0100
@@ -0,0 +1,27 @@
+name: macOS
+on: [push, pull_request]
+
+jobs:
+  tests:
+    runs-on: macos-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
+
+    steps:
+    - uses: actions/checkout@v6
+
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v6
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Run tests
+      run: |
+        pip install -U pip
+        pip install -U tox
+        tox -e py
+
+    - name: Upload coverage report
+      uses: codecov/codecov-action@v5
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/.github/workflows/tests-ubuntu.yml 
new/cssselect-1.4.0/.github/workflows/tests-ubuntu.yml
--- old/cssselect-1.3.0/.github/workflows/tests-ubuntu.yml      1970-01-01 
01:00:00.000000000 +0100
+++ new/cssselect-1.4.0/.github/workflows/tests-ubuntu.yml      2026-01-29 
07:59:11.000000000 +0100
@@ -0,0 +1,33 @@
+name: Ubuntu
+on: [push, pull_request]
+
+jobs:
+  tests:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.11"]
+
+    steps:
+    - uses: actions/checkout@v6
+
+    - name: Install system libraries
+      if: contains(matrix.python-version, 'pypy')
+      run: |
+        sudo apt-get update
+        sudo apt-get install libxml2-dev libxslt-dev
+
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v6
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Run tests
+      run: |
+        pip install -U pip
+        pip install -U tox
+        tox -e py
+
+    - name: Upload coverage report
+      uses: codecov/codecov-action@v5
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/.github/workflows/tests-windows.yml 
new/cssselect-1.4.0/.github/workflows/tests-windows.yml
--- old/cssselect-1.3.0/.github/workflows/tests-windows.yml     1970-01-01 
01:00:00.000000000 +0100
+++ new/cssselect-1.4.0/.github/workflows/tests-windows.yml     2026-01-29 
07:59:11.000000000 +0100
@@ -0,0 +1,27 @@
+name: Windows
+on: [push, pull_request]
+
+jobs:
+  tests:
+    runs-on: windows-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
+
+    steps:
+    - uses: actions/checkout@v6
+
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v6
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Run tests
+      run: |
+        pip install -U pip
+        pip install -U tox
+        tox -e py
+
+    - name: Upload coverage report
+      uses: codecov/codecov-action@v5
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/.github/workflows/tests.yml 
new/cssselect-1.4.0/.github/workflows/tests.yml
--- old/cssselect-1.3.0/.github/workflows/tests.yml     2025-03-10 
10:20:12.000000000 +0100
+++ new/cssselect-1.4.0/.github/workflows/tests.yml     1970-01-01 
01:00:00.000000000 +0100
@@ -1,27 +0,0 @@
-name: Tests
-on: [push, pull_request]
-
-jobs:
-  tests:
-    runs-on: ubuntu-latest
-    strategy:
-      fail-fast: false
-      matrix:
-        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.10"]
-
-    steps:
-    - uses: actions/checkout@v4
-
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v5
-      with:
-        python-version: ${{ matrix.python-version }}
-
-    - name: Run tests
-      run: |
-        pip install -U pip
-        pip install -U tox
-        tox -e py
-
-    - name: Upload coverage report
-      uses: codecov/codecov-action@v5
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/.pre-commit-config.yaml 
new/cssselect-1.4.0/.pre-commit-config.yaml
--- old/cssselect-1.3.0/.pre-commit-config.yaml 2025-03-10 10:20:12.000000000 
+0100
+++ new/cssselect-1.4.0/.pre-commit-config.yaml 2026-01-29 07:59:11.000000000 
+0100
@@ -1,7 +1,26 @@
 repos:
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.9.10
+  rev: v0.14.4
   hooks:
-    - id: ruff
+    - id: ruff-check
       args: [ --fix ]
     - id: ruff-format
+- repo: https://github.com/adamchainz/blacken-docs
+  rev: 1.20.0
+  hooks:
+  - id: blacken-docs
+    additional_dependencies:
+    - black==26.1.0
+- repo: https://github.com/pre-commit/pre-commit-hooks
+  rev: v6.0.0
+  hooks:
+  - id: end-of-file-fixer
+  - id: trailing-whitespace
+- repo: https://github.com/sphinx-contrib/sphinx-lint
+  rev: v1.0.0
+  hooks:
+    - id: sphinx-lint
+- repo: https://github.com/rhysd/actionlint
+  rev: v1.7.10
+  hooks:
+    - id: actionlint
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/.readthedocs.yml 
new/cssselect-1.4.0/.readthedocs.yml
--- old/cssselect-1.3.0/.readthedocs.yml        2025-03-10 10:20:12.000000000 
+0100
+++ new/cssselect-1.4.0/.readthedocs.yml        2026-01-29 07:59:11.000000000 
+0100
@@ -8,7 +8,7 @@
   tools:
     # For available versions, see:
     # 
https://docs.readthedocs.io/en/stable/config-file/v2.html#build-tools-python
-    python: "3.13"  # Keep in sync with .github/workflows/checks.yml
+    python: "3.14"  # Keep in sync with .github/workflows/checks.yml
 python:
   install:
     - requirements: docs/requirements.txt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/CHANGES new/cssselect-1.4.0/CHANGES
--- old/cssselect-1.3.0/CHANGES 2025-03-10 10:20:12.000000000 +0100
+++ new/cssselect-1.4.0/CHANGES 2026-01-29 07:59:11.000000000 +0100
@@ -1,6 +1,19 @@
 Changelog
 =========
 
+Version 1.4.0
+-------------
+
+Released on 2026-01-29.
+
+*   Dropped support for Python 3.9 and PyPy 3.10.
+
+*   Added support for Python 3.14 and PyPy 3.11.
+
+*   Switched the build system to ``hatchling``.
+
+*   CI fixes and improvements.
+
 Version 1.3.0
 -------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/MANIFEST.in 
new/cssselect-1.4.0/MANIFEST.in
--- old/cssselect-1.3.0/MANIFEST.in     2025-03-10 10:20:12.000000000 +0100
+++ new/cssselect-1.4.0/MANIFEST.in     1970-01-01 01:00:00.000000000 +0100
@@ -1,4 +0,0 @@
-include AUTHORS CHANGES LICENSE README.rst tox.ini cssselect/py.typed
-recursive-include docs *
-recursive-include tests *
-prune docs/_build
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/README.rst 
new/cssselect-1.4.0/README.rst
--- old/cssselect-1.3.0/README.rst      2025-03-10 10:20:12.000000000 +0100
+++ new/cssselect-1.4.0/README.rst      2026-01-29 07:59:11.000000000 +0100
@@ -11,8 +11,8 @@
    :target: https://pypi.python.org/pypi/cssselect
    :alt: Supported Python Versions
 
-.. image:: 
https://github.com/scrapy/cssselect/actions/workflows/tests.yml/badge.svg
-   :target: https://github.com/scrapy/cssselect/actions/workflows/tests.yml
+.. image:: 
https://github.com/scrapy/cssselect/actions/workflows/tests-ubuntu.yml/badge.svg
+   :target: 
https://github.com/scrapy/cssselect/actions/workflows/tests-ubuntu.yml
    :alt: Tests
 
 .. image:: https://img.shields.io/codecov/c/github/scrapy/cssselect/master.svg
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/cssselect/__init__.py 
new/cssselect-1.4.0/cssselect/__init__.py
--- old/cssselect-1.3.0/cssselect/__init__.py   2025-03-10 10:20:12.000000000 
+0100
+++ new/cssselect-1.4.0/cssselect/__init__.py   2026-01-29 07:59:11.000000000 
+0100
@@ -32,5 +32,5 @@
     "parse",
 )
 
-VERSION = "1.3.0"
+VERSION = "1.4.0"
 __version__ = VERSION
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/cssselect/parser.py 
new/cssselect-1.4.0/cssselect/parser.py
--- old/cssselect-1.3.0/cssselect/parser.py     2025-03-10 10:20:12.000000000 
+0100
+++ new/cssselect-1.4.0/cssselect/parser.py     2026-01-29 07:59:11.000000000 
+0100
@@ -16,7 +16,7 @@
 import operator
 import re
 import sys
-from typing import TYPE_CHECKING, Literal, Optional, Protocol, Union, cast, 
overload
+from typing import TYPE_CHECKING, Literal, Protocol, TypeAlias, Union, cast, 
overload
 
 if TYPE_CHECKING:
     from collections.abc import Iterable, Iterator, Sequence
@@ -46,7 +46,7 @@
 
 #### Parsed objects
 
-Tree = Union[
+Tree: TypeAlias = Union[
     "Element",
     "Hash",
     "Class",
@@ -59,7 +59,7 @@
     "SpecificityAdjustment",
     "CombinedSelector",
 ]
-PseudoElement = Union["FunctionalPseudoElement", str]
+PseudoElement: TypeAlias = Union["FunctionalPseudoElement", str]
 
 
 class Selector:
@@ -441,7 +441,7 @@
     Represents selector#id
     """
 
-    def __init__(self, selector: Tree, id: str) -> None:
+    def __init__(self, selector: Tree, id: str) -> None:  # noqa: A002
         self.selector = selector
         self.id = id
 
@@ -562,7 +562,7 @@
             )
         if peek.is_delim("+", ">", "~"):
             # A combinator
-            combinator = cast(str, stream.next().value)
+            combinator = cast("str", stream.next().value)
             stream.skip_whitespace()
         else:
             # By exclusion, the last parse_simple_selector() ended
@@ -608,7 +608,7 @@
                 f"Got pseudo-element ::{pseudo_element} not at the end of a 
selector"
             )
         if peek.type == "HASH":
-            result = Hash(result, cast(str, stream.next().value))
+            result = Hash(result, cast("str", stream.next().value))
         elif peek == ("DELIM", "."):
             stream.next()
             result = Class(result, stream.next_ident())
@@ -660,13 +660,13 @@
                 argument, argument_pseudo_element = parse_simple_selector(
                     stream, inside_negation=True
                 )
-                next = stream.next()
+                next_ = stream.next()
                 if argument_pseudo_element:
                     raise SelectorSyntaxError(
-                        f"Got pseudo-element ::{argument_pseudo_element} 
inside :not() at {next.pos}"
+                        f"Got pseudo-element ::{argument_pseudo_element} 
inside :not() at {next_.pos}"
                     )
-                if next != ("DELIM", ")"):
-                    raise SelectorSyntaxError(f"Expected ')', got {next}")
+                if next_ != ("DELIM", ")"):
+                    raise SelectorSyntaxError(f"Expected ')', got {next_}")
                 result = Negation(result, argument)
             elif ident.lower() == "has":
                 combinator, arguments = parse_relative_selector(stream)
@@ -687,46 +687,46 @@
     return result, pseudo_element
 
 
-def parse_arguments(stream: TokenStream) -> list[Token]:
+def parse_arguments(stream: TokenStream) -> list[Token]:  # noqa: RET503
     arguments: list[Token] = []
-    while 1:  # noqa: RET503
+    while 1:
         stream.skip_whitespace()
-        next = stream.next()
-        if next.type in ("IDENT", "STRING", "NUMBER") or next in [
+        next_ = stream.next()
+        if next_.type in ("IDENT", "STRING", "NUMBER") or next_ in [
             ("DELIM", "+"),
             ("DELIM", "-"),
         ]:
-            arguments.append(next)
-        elif next == ("DELIM", ")"):
+            arguments.append(next_)
+        elif next_ == ("DELIM", ")"):
             return arguments
         else:
-            raise SelectorSyntaxError(f"Expected an argument, got {next}")
+            raise SelectorSyntaxError(f"Expected an argument, got {next_}")
 
 
-def parse_relative_selector(stream: TokenStream) -> tuple[Token, Selector]:
+def parse_relative_selector(stream: TokenStream) -> tuple[Token, Selector]:  # 
noqa: RET503
     stream.skip_whitespace()
     subselector = ""
-    next = stream.next()
+    next_ = stream.next()
 
-    if next in [("DELIM", "+"), ("DELIM", "-"), ("DELIM", ">"), ("DELIM", 
"~")]:
-        combinator = next
+    if next_ in [("DELIM", "+"), ("DELIM", "-"), ("DELIM", ">"), ("DELIM", 
"~")]:
+        combinator = next_
         stream.skip_whitespace()
-        next = stream.next()
+        next_ = stream.next()
     else:
         combinator = Token("DELIM", " ", pos=0)
 
-    while 1:  # noqa: RET503
-        if next.type in ("IDENT", "STRING", "NUMBER") or next in [
+    while 1:
+        if next_.type in ("IDENT", "STRING", "NUMBER") or next_ in [
             ("DELIM", "."),
             ("DELIM", "*"),
         ]:
-            subselector += cast(str, next.value)
-        elif next == ("DELIM", ")"):
+            subselector += cast("str", next_.value)
+        elif next_ == ("DELIM", ")"):
             result = parse(subselector)
             return combinator, result[0]
         else:
-            raise SelectorSyntaxError(f"Expected an argument, got {next}")
-        next = stream.next()
+            raise SelectorSyntaxError(f"Expected an argument, got {next_}")
+        next_ = stream.next()
 
 
 def parse_simple_selector_arguments(stream: TokenStream) -> list[Tree]:
@@ -738,16 +738,16 @@
                 f"Got pseudo-element ::{pseudo_element} inside function"
             )
         stream.skip_whitespace()
-        next = stream.next()
-        if next in (("EOF", None), ("DELIM", ",")):
+        next_ = stream.next()
+        if next_ in (("EOF", None), ("DELIM", ",")):
             stream.next()
             stream.skip_whitespace()
             arguments.append(result)
-        elif next == ("DELIM", ")"):
+        elif next_ == ("DELIM", ")"):
             arguments.append(result)
             break
         else:
-            raise SelectorSyntaxError(f"Expected an argument, got {next}")
+            raise SelectorSyntaxError(f"Expected an argument, got {next_}")
     return arguments
 
 
@@ -772,27 +772,27 @@
         namespace = op = None
     if op is None:
         stream.skip_whitespace()
-        next = stream.next()
-        if next == ("DELIM", "]"):
-            return Attrib(selector, namespace, cast(str, attrib), "exists", 
None)
-        if next == ("DELIM", "="):
+        next_ = stream.next()
+        if next_ == ("DELIM", "]"):
+            return Attrib(selector, namespace, cast("str", attrib), "exists", 
None)
+        if next_ == ("DELIM", "="):
             op = "="
-        elif next.is_delim("^", "$", "*", "~", "|", "!") and (
+        elif next_.is_delim("^", "$", "*", "~", "|", "!") and (
             stream.peek() == ("DELIM", "=")
         ):
-            op = cast(str, next.value) + "="
+            op = cast("str", next_.value) + "="
             stream.next()
         else:
-            raise SelectorSyntaxError(f"Operator expected, got {next}")
+            raise SelectorSyntaxError(f"Operator expected, got {next_}")
     stream.skip_whitespace()
     value = stream.next()
     if value.type not in ("IDENT", "STRING"):
         raise SelectorSyntaxError(f"Expected string or ident, got {value}")
     stream.skip_whitespace()
-    next = stream.next()
-    if next != ("DELIM", "]"):
-        raise SelectorSyntaxError(f"Expected ']', got {next}")
-    return Attrib(selector, namespace, cast(str, attrib), op, value)
+    next_ = stream.next()
+    if next_ != ("DELIM", "]"):
+        raise SelectorSyntaxError(f"Expected ']', got {next_}")
+    return Attrib(selector, namespace, cast("str", attrib), op, value)
 
 
 def parse_series(tokens: Iterable[Token]) -> tuple[int, int]:
@@ -806,7 +806,7 @@
     for token in tokens:
         if token.type == "STRING":
             raise ValueError("String tokens not allowed in series.")
-    s = "".join(cast(str, token.value) for token in tokens).strip()
+    s = "".join(cast("str", token.value) for token in tokens).strip()
     if s == "odd":
         return 2, 1
     if s == "even":
@@ -831,7 +831,7 @@
 #### Token objects
 
 
-class Token(tuple[str, Optional[str]]):  # noqa: SLOT001
+class Token(tuple[str, str | None]):  # noqa: SLOT001
     @overload
     def __new__(
         cls,
@@ -867,7 +867,7 @@
     def css(self) -> str:
         if self.type == "STRING":
             return repr(self.value)
-        return cast(str, self.value)
+        return cast("str", self.value)
 
 
 class EOFToken(Token):
@@ -1015,9 +1015,9 @@
             assert self.peeked is not None
             self.used.append(self.peeked)
             return self.peeked
-        next = self.next_token()
-        self.used.append(next)
-        return next
+        next_ = self.next_token()
+        self.used.append(next_)
+        return next_
 
     def peek(self) -> Token:
         if not self._peeking:
@@ -1027,18 +1027,18 @@
         return self.peeked
 
     def next_ident(self) -> str:
-        next = self.next()
-        if next.type != "IDENT":
-            raise SelectorSyntaxError(f"Expected ident, got {next}")
-        return cast(str, next.value)
+        next_ = self.next()
+        if next_.type != "IDENT":
+            raise SelectorSyntaxError(f"Expected ident, got {next_}")
+        return cast("str", next_.value)
 
     def next_ident_or_star(self) -> str | None:
-        next = self.next()
-        if next.type == "IDENT":
-            return next.value
-        if next == ("DELIM", "*"):
+        next_ = self.next()
+        if next_.type == "IDENT":
+            return next_.value
+        if next_ == ("DELIM", "*"):
             return None
-        raise SelectorSyntaxError(f"Expected ident or '*', got {next}")
+        raise SelectorSyntaxError(f"Expected ident or '*', got {next_}")
 
     def skip_whitespace(self) -> None:
         peek = self.peek()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/cssselect/xpath.py 
new/cssselect-1.4.0/cssselect/xpath.py
--- old/cssselect-1.3.0/cssselect/xpath.py      2025-03-10 10:20:12.000000000 
+0100
+++ new/cssselect-1.4.0/cssselect/xpath.py      2026-01-29 07:59:11.000000000 
+0100
@@ -14,8 +14,7 @@
 from __future__ import annotations
 
 import re
-from collections.abc import Callable
-from typing import TYPE_CHECKING, Optional, cast
+from typing import TYPE_CHECKING, cast
 
 from cssselect.parser import (
     Attrib,
@@ -38,6 +37,8 @@
 )
 
 if TYPE_CHECKING:
+    from collections.abc import Callable
+
     # typing.Self requires Python 3.11
     from typing_extensions import Self
 
@@ -289,7 +290,7 @@
         """Translate any parsed selector object."""
         type_name = type(parsed_selector).__name__
         method = cast(
-            Optional[Callable[[Tree], XPathExpr]],
+            "Callable[[Tree], XPathExpr] | None",
             getattr(self, f"xpath_{type_name.lower()}", None),
         )
         if method is None:
@@ -302,7 +303,7 @@
         """Translate a combined selector."""
         combinator = self.combinator_mapping[combined.combinator]
         method = cast(
-            Callable[[XPathExpr, XPathExpr], XPathExpr],
+            "Callable[[XPathExpr, XPathExpr], XPathExpr]",
             getattr(self, f"xpath_{combinator}_combinator"),
         )
         return method(self.xpath(combined.selector), 
self.xpath(combined.subselector))
@@ -321,10 +322,10 @@
         subselector = relation.subselector
         right = self.xpath(subselector.parsed_tree)
         method = cast(
-            Callable[[XPathExpr, XPathExpr], XPathExpr],
+            "Callable[[XPathExpr, XPathExpr], XPathExpr]",
             getattr(
                 self,
-                f"xpath_relation_{self.combinator_mapping[cast(str, 
combinator.value)]}_combinator",
+                f"xpath_relation_{self.combinator_mapping[cast('str', 
combinator.value)]}_combinator",
             ),
         )
         return method(xpath, right)
@@ -351,7 +352,7 @@
         """Translate a functional pseudo-class."""
         method_name = "xpath_{}_function".format(function.name.replace("-", 
"_"))
         method = cast(
-            Optional[Callable[[XPathExpr, Function], XPathExpr]],
+            "Callable[[XPathExpr, Function], XPathExpr] | None",
             getattr(self, method_name, None),
         )
         if not method:
@@ -362,7 +363,8 @@
         """Translate a pseudo-class."""
         method_name = "xpath_{}_pseudo".format(pseudo.ident.replace("-", "_"))
         method = cast(
-            Optional[Callable[[XPathExpr], XPathExpr]], getattr(self, 
method_name, None)
+            "Callable[[XPathExpr], XPathExpr] | None",
+            getattr(self, method_name, None),
         )
         if not method:
             # TODO: better error message for pseudo-elements?
@@ -373,7 +375,7 @@
         """Translate an attribute selector."""
         operator = self.attribute_operator_mapping[selector.operator]
         method = cast(
-            Callable[[XPathExpr, str, Optional[str]], XPathExpr],
+            "Callable[[XPathExpr, str, str | None], XPathExpr]",
             getattr(self, f"xpath_attrib_{operator}"),
         )
         if self.lower_case_attribute_names:
@@ -391,7 +393,7 @@
         if selector.value is None:
             value = None
         elif self.lower_case_attribute_values:
-            value = cast(str, selector.value.value).lower()
+            value = cast("str", selector.value.value).lower()
         else:
             value = selector.value.value
         return method(self.xpath(selector.selector), attrib, value)
@@ -645,7 +647,7 @@
             raise ExpressionError(
                 f"Expected a single string or ident for :contains(), got 
{function.arguments!r}"
             )
-        value = cast(str, function.arguments[0].value)
+        value = cast("str", function.arguments[0].value)
         return xpath.add_condition(f"contains(., {self.xpath_literal(value)})")
 
     def xpath_lang_function(self, xpath: XPathExpr, function: Function) -> 
XPathExpr:
@@ -653,7 +655,7 @@
             raise ExpressionError(
                 f"Expected a single string or ident for :lang(), got 
{function.arguments!r}"
             )
-        value = cast(str, function.arguments[0].value)
+        value = cast("str", function.arguments[0].value)
         return xpath.add_condition(f"lang({self.xpath_literal(value)})")
 
     # Pseudo: dispatch by pseudo-class name
@@ -823,7 +825,7 @@
             self.lower_case_element_names = True
             self.lower_case_attribute_names = True
 
-    def xpath_checked_pseudo(self, xpath: XPathExpr) -> XPathExpr:  # type: 
ignore[override]
+    def xpath_checked_pseudo(self, xpath: XPathExpr) -> XPathExpr:
         # FIXME: is this really all the elements?
         return xpath.add_condition(
             "(@selected and name(.) = 'option') or "
@@ -848,7 +850,7 @@
             f"'-'), {arg})]"
         )
 
-    def xpath_link_pseudo(self, xpath: XPathExpr) -> XPathExpr:  # type: 
ignore[override]
+    def xpath_link_pseudo(self, xpath: XPathExpr) -> XPathExpr:
         return xpath.add_condition(
             "@href and (name(.) = 'a' or name(.) = 'link' or name(.) = 'area')"
         )
@@ -856,7 +858,7 @@
     # Links are never visited, the implementation for :visited is the same
     # as in GenericTranslator
 
-    def xpath_disabled_pseudo(self, xpath: XPathExpr) -> XPathExpr:  # type: 
ignore[override]
+    def xpath_disabled_pseudo(self, xpath: XPathExpr) -> XPathExpr:
         # http://www.w3.org/TR/html5/section-index.html#attributes-1
         return xpath.add_condition(
             """
@@ -886,7 +888,7 @@
         # FIXME: in the second half, add "and is not a descendant of that
         # fieldset element's first legend element child, if any."
 
-    def xpath_enabled_pseudo(self, xpath: XPathExpr) -> XPathExpr:  # type: 
ignore[override]
+    def xpath_enabled_pseudo(self, xpath: XPathExpr) -> XPathExpr:
         # http://www.w3.org/TR/html5/section-index.html#attributes-1
         return xpath.add_condition(
             """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/docs/conf.py 
new/cssselect-1.4.0/docs/conf.py
--- old/cssselect-1.3.0/docs/conf.py    2025-03-10 10:20:12.000000000 +0100
+++ new/cssselect-1.4.0/docs/conf.py    2026-01-29 07:59:11.000000000 +0100
@@ -32,7 +32,7 @@
 templates_path = ["_templates"]
 
 # The suffix of source filenames.
-source_suffix = ".rst"
+source_suffix = {".rst": "restructuredtext"}
 
 # The encoding of source files.
 # source_encoding = 'utf-8-sig'
@@ -42,7 +42,7 @@
 
 # General information about the project.
 project = "cssselect"
-copyright = "2012-2017, Simon Sapin, Scrapy developers"
+project_copyright = "2012-2017, Simon Sapin, Scrapy developers"
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/pyproject.toml 
new/cssselect-1.4.0/pyproject.toml
--- old/cssselect-1.3.0/pyproject.toml  2025-03-10 10:20:12.000000000 +0100
+++ new/cssselect-1.4.0/pyproject.toml  2026-01-29 07:59:11.000000000 +0100
@@ -1,11 +1,66 @@
+[build-system]
+build-backend = "hatchling.build"
+requires = ["hatchling>=1.27.0"]
+
+[project]
+name = "cssselect"
+license = "BSD-3-Clause"
+license-files = ["LICENSE", "AUTHORS"]
+description = "cssselect parses CSS3 Selectors and translates them to XPath 
1.0"
+readme = "README.rst"
+authors = [{ name = "Ian Bicking", email = "[email protected]" }]
+maintainers = [{ name = "Paul Tremberth", email = "[email protected]" }]
+requires-python = ">=3.10"
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "Intended Audience :: Developers",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3.14",
+    "Programming Language :: Python :: Implementation :: CPython",
+    "Programming Language :: Python :: Implementation :: PyPy",
+]
+dynamic = ["version"]
+
+[project.urls]
+"Homepage" = "https://github.com/scrapy/cssselect";
+
+[tool.hatch.version]
+path = "cssselect/__init__.py"
+
+[tool.hatch.build.targets.sdist]
+include = [
+    "/cssselect",
+    "/docs",
+    "/tests",
+    "/CHANGES",
+    "/README.rst",
+    "/tox.ini",
+]
+exclude = [
+    "/docs/_build",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["cssselect"]
+
 [tool.bumpversion]
-current_version = "1.3.0"
+current_version = "1.4.0"
 commit = true
 tag = true
 
 [[tool.bumpversion.files]]
 filename = "cssselect/__init__.py"
 
+[[tool.bumpversion.files]]
+filename = "CHANGES"
+search = "^Unreleased\\.$"
+replace = "Released on {now:%Y-%m-%d}."
+regex = true
+
 [tool.coverage.run]
 branch = true
 source = ["cssselect"]
@@ -15,9 +70,11 @@
     "def __repr__",
     "if sys.version_info",
     "if __name__ == '__main__':",
-    "if TYPE_CHECKING:",
 ]
 
+[tool.mypy]
+strict = true
+
 [tool.pylint.MASTER]
 persistent = "no"
 extension-pkg-allow-list = ["lxml"]
@@ -55,10 +112,16 @@
 
 [tool.ruff.lint]
 extend-select = [
+    # flake8-builtins
+    "A",
+    # flake8-async
+    "ASYNC",
     # flake8-bugbear
     "B",
     # flake8-comprehensions
     "C4",
+    # flake8-commas
+    "COM",
     # pydocstyle
     "D",
     # flake8-future-annotations
@@ -81,6 +144,8 @@
     "PIE",
     # pylint
     "PL",
+    # flake8-pytest-style
+    "PT",
     # flake8-use-pathlib
     "PTH",
     # flake8-pyi
@@ -111,6 +176,8 @@
     "YTT",
 ]
 ignore = [
+    # Trailing comma missing
+    "COM812",
     # Missing docstring in public module
     "D100",
     # Missing docstring in public class
@@ -163,9 +230,10 @@
     "RUF012",
     # Use of `assert` detected
     "S101",
-    # Using lxml to parse untrusted data is known to be vulnerable to XML 
attacks
-    "S320",
 ]
 
+[tool.ruff.lint.isort]
+split-on-trailing-comma = false
+
 [tool.ruff.lint.pydocstyle]
 convention = "pep257"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/setup.py new/cssselect-1.4.0/setup.py
--- old/cssselect-1.3.0/setup.py        2025-03-10 10:20:12.000000000 +0100
+++ new/cssselect-1.4.0/setup.py        1970-01-01 01:00:00.000000000 +0100
@@ -1,43 +0,0 @@
-import re
-from pathlib import Path
-
-from setuptools import setup
-
-ROOT = Path(__file__).parent
-README = (ROOT / "README.rst").read_text(encoding="utf-8")
-INIT_PY = (ROOT / "cssselect" / "__init__.py").read_text(encoding="utf-8")
-VERSION = re.search('VERSION = "([^"]+)"', INIT_PY).group(1)
-
-
-setup(
-    name="cssselect",
-    version=VERSION,
-    author="Ian Bicking",
-    author_email="[email protected]",
-    maintainer="Paul Tremberth",
-    maintainer_email="[email protected]",
-    description="cssselect parses CSS3 Selectors and translates them to XPath 
1.0",
-    long_description=README,
-    long_description_content_type="text/x-rst",
-    url="https://github.com/scrapy/cssselect";,
-    license="BSD",
-    packages=["cssselect"],
-    package_data={
-        "cssselect": ["py.typed"],
-    },
-    include_package_data=True,
-    python_requires=">=3.9",
-    classifiers=[
-        "Development Status :: 4 - Beta",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: BSD License",
-        "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",
-        "Programming Language :: Python :: Implementation :: CPython",
-        "Programming Language :: Python :: Implementation :: PyPy",
-    ],
-)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/tests/test_cssselect.py 
new/cssselect-1.4.0/tests/test_cssselect.py
--- old/cssselect-1.3.0/tests/test_cssselect.py 2025-03-10 10:20:12.000000000 
+0100
+++ new/cssselect-1.4.0/tests/test_cssselect.py 2026-01-29 07:59:11.000000000 
+0100
@@ -23,6 +23,7 @@
 import unittest
 from typing import TYPE_CHECKING
 
+import pytest
 from lxml import etree, html
 
 from cssselect import (
@@ -268,12 +269,8 @@
         (selector,) = parse("e::foo")
         assert selector.pseudo_element == "foo"
         assert tr.selector_to_xpath(selector, prefix="") == "e"
-        self.assertRaises(
-            ExpressionError,
-            tr.selector_to_xpath,
-            selector,
-            translate_pseudo_elements=True,
-        )
+        with pytest.raises(ExpressionError):
+            tr.selector_to_xpath(selector, translate_pseudo_elements=True)
 
         # Special test for the unicode symbols and ':scope' element if check
         # Errors if use repr() instead of __repr__()
@@ -567,19 +564,32 @@
         assert xpath(r"[h\a0 ref]") == ("*[attribute::*[name() = 'h ref']]")  
# h\xa0ref
         assert xpath(r"[h\]ref]") == ("*[attribute::*[name() = 'h]ref']]")
 
-        self.assertRaises(ExpressionError, xpath, ":fİrst-child")
-        self.assertRaises(ExpressionError, xpath, ":first-of-type")
-        self.assertRaises(ExpressionError, xpath, ":only-of-type")
-        self.assertRaises(ExpressionError, xpath, ":last-of-type")
-        self.assertRaises(ExpressionError, xpath, ":nth-of-type(1)")
-        self.assertRaises(ExpressionError, xpath, ":nth-last-of-type(1)")
-        self.assertRaises(ExpressionError, xpath, ":nth-child(n-)")
-        self.assertRaises(ExpressionError, xpath, ":after")
-        self.assertRaises(ExpressionError, xpath, ":lorem-ipsum")
-        self.assertRaises(ExpressionError, xpath, ":lorem(ipsum)")
-        self.assertRaises(ExpressionError, xpath, "::lorem-ipsum")
-        self.assertRaises(TypeError, GenericTranslator().css_to_xpath, 4)
-        self.assertRaises(TypeError, GenericTranslator().selector_to_xpath, 
"foo")
+        with pytest.raises(ExpressionError):
+            xpath(":fİrst-child")
+        with pytest.raises(ExpressionError):
+            xpath(":first-of-type")
+        with pytest.raises(ExpressionError):
+            xpath(":only-of-type")
+        with pytest.raises(ExpressionError):
+            xpath(":last-of-type")
+        with pytest.raises(ExpressionError):
+            xpath(":nth-of-type(1)")
+        with pytest.raises(ExpressionError):
+            xpath(":nth-last-of-type(1)")
+        with pytest.raises(ExpressionError):
+            xpath(":nth-child(n-)")
+        with pytest.raises(ExpressionError):
+            xpath(":after")
+        with pytest.raises(ExpressionError):
+            xpath(":lorem-ipsum")
+        with pytest.raises(ExpressionError):
+            xpath(":lorem(ipsum)")
+        with pytest.raises(ExpressionError):
+            xpath("::lorem-ipsum")
+        with pytest.raises(TypeError):
+            GenericTranslator().css_to_xpath(4)  # type: ignore[arg-type]
+        with pytest.raises(TypeError):
+            GenericTranslator().selector_to_xpath("foo")  # type: 
ignore[arg-type]
 
     def test_unicode(self) -> None:
         css = ".a\xc1b"
@@ -728,7 +738,7 @@
 
         def operator_id(selector: str) -> list[str]:
             xpath = CustomTranslator().css_to_xpath(selector)
-            items = typing.cast(list["etree._Element"], document.xpath(xpath))
+            items = typing.cast("list[etree._Element]", document.xpath(xpath))
             items.sort(key=sort_key)
             return [element.get("id", "nil") for element in items]
 
@@ -739,7 +749,9 @@
     def test_series(self) -> None:
         def series(css: str) -> tuple[int, int] | None:
             (selector,) = parse(f":nth-child({css})")
-            args = typing.cast(FunctionalPseudoElement, 
selector.parsed_tree).arguments
+            args = typing.cast(
+                "FunctionalPseudoElement", selector.parsed_tree
+            ).arguments
             try:
                 return parse_series(args)
             except ValueError:
@@ -771,7 +783,7 @@
 
         def langid(selector: str) -> list[str]:
             xpath = css_to_xpath(selector)
-            items = typing.cast(list["etree._Element"], document.xpath(xpath))
+            items = typing.cast("list[etree._Element]", document.xpath(xpath))
             items.sort(key=sort_key)
             return [element.get("id", "nil") for element in items]
 
@@ -800,7 +812,7 @@
                 self, xpath: XPathExpr, pseudo_element: PseudoElement
             ) -> XPathExpr:
                 self.argument_types += typing.cast(
-                    FunctionalPseudoElement, pseudo_element
+                    "FunctionalPseudoElement", pseudo_element
                 ).argument_types()
                 return xpath
 
@@ -827,11 +839,11 @@
 
         def select_ids(selector: str, html_only: bool) -> list[str]:
             xpath = css_to_xpath(selector)
-            items = typing.cast(list["etree._Element"], document.xpath(xpath))
+            items = typing.cast("list[etree._Element]", document.xpath(xpath))
             if html_only:
                 assert items == []
                 xpath = html_css_to_xpath(selector)
-                items = typing.cast(list["etree._Element"], 
document.xpath(xpath))
+                items = typing.cast("list[etree._Element]", 
document.xpath(xpath))
             items.sort(key=sort_key)
             return [element.get("id", "nil") for element in items]
 
@@ -965,7 +977,8 @@
         assert pcss("span:only-child") == ["foobar-span"]
         assert pcss("li div:only-child") == ["li-div"]
         assert pcss("div *:only-child") == ["li-div", "foobar-span"]
-        self.assertRaises(ExpressionError, pcss, "p *:only-of-type")
+        with pytest.raises(ExpressionError):
+            pcss("p *:only-of-type")
         assert pcss("p:only-of-type") == ["paragraph"]
         assert pcss("a:empty", "a:EMpty") == ["name-anchor"]
         assert pcss("li:empty") == ["third-li", "fourth-li", "fifth-li", 
"sixth-li"]
@@ -1065,14 +1078,14 @@
 
     def test_select_shakespeare(self) -> None:
         document = html.document_fromstring(HTML_SHAKESPEARE)
-        body = typing.cast(list["etree._Element"], document.xpath("//body"))[0]
+        body = typing.cast("list[etree._Element]", document.xpath("//body"))[0]
         css_to_xpath = GenericTranslator().css_to_xpath
 
         basestring_ = (str, bytes)
 
         def count(selector: str) -> int:
             xpath = css_to_xpath(selector)
-            results = typing.cast(list["etree._Element"], body.xpath(xpath))
+            results = typing.cast("list[etree._Element]", body.xpath(xpath))
             assert not isinstance(results, basestring_)
             found = set()
             for item in results:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cssselect-1.3.0/tox.ini new/cssselect-1.4.0/tox.ini
--- old/cssselect-1.3.0/tox.ini 2025-03-10 10:20:12.000000000 +0100
+++ new/cssselect-1.4.0/tox.ini 2026-01-29 07:59:11.000000000 +0100
@@ -4,21 +4,20 @@
 [testenv]
 deps =
     lxml>=4.4
-    pytest-cov>=2.8
+    pytest-cov>=7.0.0
     pytest>=5.4
-    setuptools
     sybil
 commands =
     pytest --cov=cssselect \
         --cov-report=term-missing --cov-report=html --cov-report=xml \
-        --verbose {posargs: cssselect tests docs}
+        {posargs: cssselect tests docs}
 
 [testenv:pylint]
 deps =
     {[testenv]deps}
-    pylint==3.3.5
+    pylint==4.0.4
 commands =
-    pylint {posargs: cssselect setup.py tests docs}
+    pylint {posargs: cssselect tests docs}
 
 [testenv:docs]
 changedir = docs
@@ -30,10 +29,10 @@
 [testenv:typing]
 deps =
     {[testenv]deps}
-    mypy==1.15.0
-    types-lxml==2025.3.4
+    mypy==1.19.1
+    types-lxml==2026.1.1
 commands =
-    mypy --strict {posargs: cssselect tests}
+    mypy {posargs: cssselect tests}
 
 [testenv:pre-commit]
 deps = pre-commit
@@ -43,8 +42,8 @@
 [testenv:twinecheck]
 basepython = python3
 deps =
-    twine==6.1.0
-    build==1.2.2.post1
+    twine==6.2.0
+    build==1.4.0
 commands =
     python -m build --sdist
     twine check dist/*

Reply via email to