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/*