Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-soupsieve for
openSUSE:Factory checked in at 2026-09-23 14:32:49
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-soupsieve (Old)
and /work/SRC/openSUSE:Factory/.python-soupsieve.new.383539 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-soupsieve"
Wed Sep 23 14:32:49 2026 rev:28 rq:1379675 version:2.9.2
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-soupsieve/python-soupsieve.changes
2026-07-07 21:00:11.676091683 +0200
+++
/work/SRC/openSUSE:Factory/.python-soupsieve.new.383539/python-soupsieve.changes
2026-09-23 14:33:35.333921489 +0200
@@ -1,0 +2,25 @@
+Tue Sep 22 09:30:32 UTC 2026 - Nico Krapp <[email protected]>
+
+- Update to 2.9.2
+ * FIX: Fix issue where :is() and :where() were not accounting for empty
+ selectors in the max selector count as they should.
+ * FIX: Fix issue where :has() was allowing empty selectors in some
+ circumstances even though it is not forgiving.
+ * FIX: Reduce selector object size when :is() and :where() contain empty
+ selectors.
+- Update to 2.9.1
+ * FIX: Correct [attr^=""], [attr$=""], and [attr*=""] to match nothing when
+ the value is empty, per CSS Selectors Level 4 substring matching, which
+ previously matched any element merely having the attribute.
+- Update to 2.9.0 (fixes CVE-2026-85999 (bsc#1281119),
+ CVE-2026-86000 (bsc#1281120))
+ * NEW: Drop Python 3.9 support.
+ * NEW: Lazy compile selector patterns to improve initial import speed.
+ * FIX: Correct :nth-child/:nth-of-type (and -last- variants) for An+B values
+ whose sequence steps onto index 0 or onto the last child
+ (e.g. :nth-child(2n-2), :nth-child(n-1), :nth-child(n+5)), which
+ previously matched the wrong elements or nothing at all.
+ * FIX: More efficient CSS ID matching.
+ * FIX: Fix inefficient trimming of comments and white space.
+
+-------------------------------------------------------------------
Old:
----
soupsieve-2.8.4.tar.gz
New:
----
soupsieve-2.9.2.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-soupsieve.spec ++++++
--- /var/tmp/diff_new_pack.SEfQWL/_old 2026-09-23 14:33:36.258960161 +0200
+++ /var/tmp/diff_new_pack.SEfQWL/_new 2026-09-23 14:33:36.260960245 +0200
@@ -26,7 +26,7 @@
%endif
%{?sle15_python_module_pythons}
Name: python-soupsieve%{psuffix}
-Version: 2.8.4
+Version: 2.9.2
Release: 0
Summary: A modern CSS selector implementation for BeautifulSoup
License: MIT
++++++ soupsieve-2.8.4.tar.gz -> soupsieve-2.9.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/PKG-INFO new/soupsieve-2.9.2/PKG-INFO
--- old/soupsieve-2.8.4/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/soupsieve-2.9.2/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: soupsieve
-Version: 2.8.4
+Version: 2.9.2
Summary: A modern CSS selector implementation for Beautiful Soup.
Project-URL: Homepage, https://github.com/facelessuser/soupsieve
Author-email: Isaac Muse <[email protected]>
@@ -13,7 +13,6 @@
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
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
@@ -22,7 +21,7 @@
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
-Requires-Python: >=3.9
+Requires-Python: >=3.10
Description-Content-Type: text/markdown
[![Donate via PayPal][donate-image]][donate-link]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/docs/src/markdown/about/changelog.md
new/soupsieve-2.9.2/docs/src/markdown/about/changelog.md
--- old/soupsieve-2.8.4/docs/src/markdown/about/changelog.md 2020-02-02
01:00:00.000000000 +0100
+++ new/soupsieve-2.9.2/docs/src/markdown/about/changelog.md 2020-02-02
01:00:00.000000000 +0100
@@ -3,6 +3,30 @@
---
# Changelog
+## 2.9.2
+
+- **FIX**: Fix issue where `:is()` and `:where()` were not accounting for
empty selectors in the max selector count as
+ they should (@arpitjain099).
+- **FIX**: Fix issue where `:has()` was allowing empty selectors in some
circumstances even though it is not
+ forgiving.
+- **FIX**: Reduce selector object size when `:is()` and `:where()` contain
empty selectors.
+
+## 2.9.1
+
+- **FIX**: Correct `[attr^=""]`, `[attr$=""]`, and `[attr*=""]` to match
nothing when the value is empty, per CSS
+ Selectors Level 4 substring matching, which previously matched any element
merely having the attribute
+ (@chuenchen309).
+
+## 2.9
+
+- **NEW**: Drop Python 3.9 support.
+- **NEW**: Lazy compile selector patterns to improve initial import speed.
+- **FIX**: Correct `:nth-child`/`:nth-of-type` (and `-last-` variants) for
`An+B` values whose sequence steps onto
+ index 0 or onto the last child (e.g. `:nth-child(2n-2)`,
`:nth-child(n-1)`, `:nth-child(n+5)`), which previously
+ matched the wrong elements or nothing at all (@gaoflow).
+- **FIX**: More efficient CSS ID matching (@kaimandalic).
+- **FIX**: Fix inefficient trimming of comments and white space
(@kaimandalic).
+
## 2.8.4
- **FIX**: Fix another inefficient attribute pattern (@mauriceng98).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/docs/src/markdown/faq.md
new/soupsieve-2.9.2/docs/src/markdown/faq.md
--- old/soupsieve-2.8.4/docs/src/markdown/faq.md 2020-02-02
01:00:00.000000000 +0100
+++ new/soupsieve-2.9.2/docs/src/markdown/faq.md 2020-02-02
01:00:00.000000000 +0100
@@ -32,7 +32,7 @@
pseudo-classes behaved in unexpected ways without awareness to `iframes`, this
was fixed in 1.9.1. Pseudo-classes such
as [`:default`](./selectors/pseudo-classes.md#:default),
[`:indeterminate`](./selectors/pseudo-classes.md#:indeterminate),
[`:dir()`](./selectors/pseudo-classes.md#:dir),
[`:lang()`](./selectors/pseudo-classes.md#:lang),
-[`:root`](./selectors/pseudo-classes.md#:root), and
[`:contains()`](./selectors/pseudo-classes.md#:contains) were
+[`:root`](./selectors/pseudo-classes.md#:root), and
[`:contains()`](./selectors/pseudo-classes.md#:-soup-contains) were
given awareness of `iframes` to ensure they behaved properly and returned the
expected elements. This doesn't mean that
`select` won't return elements in `iframes`, but it won't allow something like
`:default` to select a `button` in an
`iframe` whose parent `form` is outside the `iframe`. Or better put, a default
`button` will be evaluated in the context
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/docs/src/markdown/selectors/index.md
new/soupsieve-2.9.2/docs/src/markdown/selectors/index.md
--- old/soupsieve-2.8.4/docs/src/markdown/selectors/index.md 2020-02-02
01:00:00.000000000 +0100
+++ new/soupsieve-2.9.2/docs/src/markdown/selectors/index.md 2020-02-02
01:00:00.000000000 +0100
@@ -96,20 +96,21 @@
### Simple Selector
-A simple selector represents a single condition on an element. It can be a
[type selector](#type-selectors),
-[universal selector](#universal-selectors), [ID selector](#id-selectors),
[class selector](#class-selectors),
-[attribute selector](#attribute-selectors), or [pseudo class
selector](#pseudo-classes).
+A simple selector represents a single condition on an element. It can be a
[type selector](./basic.md#type-selectors),
+[universal selector](./basic.md#universal-selectors), [ID
selector](./basic.md#id-selectors),
+[class selector](./basic.md#class-selectors), [attribute
selector](./basic.md#attribute-selectors), or
+[pseudo class selector](./pseudo-classes.md).
### Compound Selector
A [compound](#compound-selector) selector is a sequence of
[simple](#simple-selector) selectors. They do not contain any
-[combinators](#combinators-and-selector-lists). If a universal or type
selector is used, they must come first, and only
-one instance of either a universal or type selector can be used, both cannot
be used at the same time.
+[combinators](./combinators.md). If a universal or type selector is used, they
must come first, and only one instance of
+either a universal or type selector can be used, both cannot be used at the
same time.
### Complex Selector
A complex selector consists of multiple [simple](#simple-selector) or
[compound](#compound-selector) selectors joined
-with [combinators](#combinators-and-selector-lists).
+with [combinators](./combinators.md).
### Selector List
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/soupsieve-2.8.4/docs/src/markdown/selectors/pseudo-classes.md
new/soupsieve-2.9.2/docs/src/markdown/selectors/pseudo-classes.md
--- old/soupsieve-2.8.4/docs/src/markdown/selectors/pseudo-classes.md
2020-02-02 01:00:00.000000000 +0100
+++ new/soupsieve-2.9.2/docs/src/markdown/selectors/pseudo-classes.md
2020-02-02 01:00:00.000000000 +0100
@@ -4,9 +4,9 @@
These are pseudo classes that are either fully or partially supported. Partial
support is usually due to limitations of
not being in a live, browser environment. Pseudo classes that cannot be
implemented are found under
-[Non-Applicable Pseudo
Classes](./unsupported.md/#non-applicable-pseudo-classes). Any selectors that
are not found here or under the
-non-applicable either are under consideration, have not yet been evaluated, or
are too new and viewed as a risk to
-implement as they might not stick around.
+[Non-Applicable Pseudo Classes](./unsupported.md). Any selectors that are not
found here or under the non-applicable
+either are under consideration, have not yet been evaluated, or are too new
and viewed as a risk to implement as they
+might not stick around.
## `:any-link`:material-language-html5:{: title="HTML"
data-md-color-primary="orange" .icon} {:#:any-link}
@@ -417,8 +417,8 @@
Selects an element if any of the relative selectors passed as parameters
(which are relative to the `:scope` of the
given element), match at least one element.
-While the level 4 specifications state that [compound](#compound-selector)
selectors are supported, complex selectors
-are planned for level 5 CSS selectors. Soup Sieve supports
[complex](#complex-selector) selectors.
+While the level 4 specifications state that
[compound](./index.md#compound-selector) selectors are supported, complex
+selectors are planned for level 5 CSS selectors. Soup Sieve supports
[complex](./index.md#complex-selector) selectors.
In addition to supporting complex selectors, Soup Sieve also supports nested
`:has()` which has been excluded from the
level 4 specifications to help encourage browsers to implement `:has()`. This
exclusion helps to reduces complexity and
@@ -557,9 +557,9 @@
The alias `:matches()` is also supported as it was the original name for the
selector, and some browsers support it.
It is strongly encouraged to use `:is()` instead as support for `:matches()`
may be dropped in the future.
-While the level 4 specifications state that [compound](#compound-selector)
selectors are supported, some browsers
-(Safari) support complex selectors which are planned for level 5 CSS
selectors. Soup Sieve also supports
-[complex](#complex-selector) selectors.
+While the level 4 specifications state that
[compound](./index.md#compound-selector) selectors are supported, some
+browsers (Safari) support complex selectors which are planned for level 5 CSS
selectors. Soup Sieve also supports
+[complex](./index.md#complex-selector) selectors.
/// tab | Syntax
```css
@@ -880,8 +880,9 @@
Level 4+ CSS:material-flask:{: title="Experimental"
data-md-color-primary="purple" .icon}
- Selects all elements that do not match any of the selectors in the
selector list. While the level 4 specifications
- state that [compound](#compound-selector) selectors are supported, some
browsers (Safari) support complex selectors
- which are planned for level 5 CSS selectors. Soup Sieve also supports
[complex](#complex-selector) selectors.
+ state that [compound](./index.md#compound-selector) selectors are
supported, some browsers (Safari) support complex
+ selectors which are planned for level 5 CSS selectors. Soup Sieve also
supports
+ [complex](./index.md#complex-selector) selectors.
/// tab | Syntax
```css
@@ -977,10 +978,11 @@
Essentially, `#!css img:nth-of-type(2)` would be equivalent to `#!css
:nth-child(2 of img)`. The advantage of using
`:nth-child(an+b [of S]?)` over `:nth-of-type` is that `:nth-of-type` is
restricted to types, while
- `:nth-child(an+b [of S]?)` can use [complex](#complex-selector) selectors.
+ `:nth-child(an+b [of S]?)` can use [complex](./index.md#complex-selector)
selectors.
- While the level 4 specifications state that [compound](#compound-selector)
selectors are supported, complex
- selectors are planned for level 5 CSS selectors. Soup Sieve supports
[complex](#complex-selector) selectors.
+ While the level 4 specifications state that
[compound](./index.md#compound-selector) selectors are supported,
+ complex selectors are planned for level 5 CSS selectors. Soup Sieve
supports [complex](./index.md#complex-selector)
+ selectors.
/// tab | Syntax
```css
@@ -1080,10 +1082,11 @@
Essentially, `#!css img:nth-last-of-type(2)` would be equivalent to `#!css
:nth-last-child(2 of img)`. The advantage
of using `:nth-last-child(an+b [of S]?)` over `:nth-last-of-type` is that
`:nth-last-of-type` is restricted to
- types, while `:nth-last-child(an+b [of S]?)` can use
[complex](#complex-selector) selectors.
+ types, while `:nth-last-child(an+b [of S]?)` can use
[complex](./index.md#complex-selector) selectors.
- While the level 4 specifications state that [compound](#compound-selector)
selectors are supported, complex
- selectors are planned for level 5 CSS selectors. Soup Sieve supports
[complex](#complex-selector) selectors.
+ While the level 4 specifications state that
[compound](./index.md#compound-selector) selectors are supported,
+ complex selectors are planned for level 5 CSS selectors. Soup Sieve
supports [complex](./index.md#complex-selector)
+ selectors.
/// tab | Syntax
```css
@@ -1671,9 +1674,9 @@
used to see which applies. Beautiful Soup and Soup Sieve don't care about
specificity so `:where()` is essentially just
an alias for `:is()`.
-While the level 4 specifications state that [compound](#compound-selector)
selectors are supported, some browsers
-(Safari) support complex selectors which are planned for level 5 CSS
selectors. Soup Sieve also supports
-[complex](#complex-selector) selectors.
+While the level 4 specifications state that
[compound](./index.md#compound-selector) selectors are supported, some
+browsers (Safari) support complex selectors which are planned for level 5 CSS
selectors. Soup Sieve also supports
+[complex](./index.md#complex-selector) selectors.
/// tab | Syntax
```css
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/hatch_build.py
new/soupsieve-2.9.2/hatch_build.py
--- old/soupsieve-2.8.4/hatch_build.py 2020-02-02 01:00:00.000000000 +0100
+++ new/soupsieve-2.9.2/hatch_build.py 2020-02-02 01:00:00.000000000 +0100
@@ -29,7 +29,6 @@
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/pyproject.toml
new/soupsieve-2.9.2/pyproject.toml
--- old/soupsieve-2.8.4/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
+++ new/soupsieve-2.9.2/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
@@ -9,7 +9,7 @@
description = "A modern CSS selector implementation for Beautiful Soup."
readme = "README.md"
license = "MIT"
-requires-python = ">=3.9"
+requires-python = ">=3.10"
authors = [
{ name = "Isaac Muse", email = "[email protected]" },
]
@@ -109,7 +109,7 @@
[tox]
isolated_build = true
envlist =
- py{39,310,311,312,313,314},
+ py{310,311,312,313,314},
lint, nolxml, nohtml5lib
[testenv]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/soupsieve/__meta__.py
new/soupsieve-2.9.2/soupsieve/__meta__.py
--- old/soupsieve-2.8.4/soupsieve/__meta__.py 2020-02-02 01:00:00.000000000
+0100
+++ new/soupsieve-2.9.2/soupsieve/__meta__.py 2020-02-02 01:00:00.000000000
+0100
@@ -193,5 +193,5 @@
return Version(major, minor, micro, release, pre, post, dev)
-__version_info__ = Version(2, 8, 4, "final")
+__version_info__ = Version(2, 9, 2, "final")
__version__ = __version_info__._get_canonical()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/soupsieve/css_match.py
new/soupsieve-2.9.2/soupsieve/css_match.py
--- old/soupsieve-2.8.4/soupsieve/css_match.py 2020-02-02 01:00:00.000000000
+0100
+++ new/soupsieve-2.9.2/soupsieve/css_match.py 2020-02-02 01:00:00.000000000
+0100
@@ -995,44 +995,18 @@
# We can only adjust bounds within a variable index
if var:
- # Abort if our nth index is out of bounds and only getting
further out of bounds as we increment.
- # Otherwise, increment to try to get in bounds.
- adjust = None
- while idx < 1 or idx > last_index:
- if idx < 0:
- diff_low = 0 - idx
- if adjust is not None and adjust == 1:
- break
- adjust = -1
- count += count_incr
- idx = last_idx = a * count + b if var else a
- diff = 0 - idx
- if diff >= diff_low:
- break
- else:
- diff_high = idx - last_index
- if adjust is not None and adjust == -1:
- break
- adjust = 1
- count += count_incr
- idx = last_idx = a * count + b if var else a
- diff = idx - last_index
- if diff >= diff_high:
- break
- diff_high = diff
-
- # If a < 0, our count is working backwards, so floor the index
by increasing the count.
- # Find the count that yields the lowest, in bound value and
use that.
- # Lastly reverse count increment so that we'll increase our
index.
- lowest = count
- if a < 0:
- while idx >= 1:
- lowest = count
- count += count_incr
- idx = last_idx = a * count + b if var else a
+ # Find the count `n` that yields the smallest in-bounds index
+ # (>= 1), then set the increment direction so that the index
+ # ascends from there as the evaluation loop below walks
children.
+ if a > 0:
+ # Ascending sequence: smallest n with a * n + b >= 1.
+ count = 0 if b >= 1 else -(-(1 - b) // a)
+ elif a < 0:
+ # Descending sequence: largest n with a * n + b >= 1, then
+ # walk n back down so the index increases.
+ count = (b - 1) // -a if b >= 1 else 0
count_incr = -1
- count = lowest
- idx = last_idx = a * count + b if var else a
+ idx = last_idx = a * count + b
# Evaluate elements while our calculated nth index is still in
range
while 1 <= idx <= last_index + 1:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/soupsieve/css_parser.py
new/soupsieve-2.9.2/soupsieve/css_parser.py
--- old/soupsieve-2.8.4/soupsieve/css_parser.py 2020-02-02 01:00:00.000000000
+0100
+++ new/soupsieve-2.9.2/soupsieve/css_parser.py 2020-02-02 01:00:00.000000000
+0100
@@ -8,6 +8,12 @@
from .util import SelectorSyntaxError
import warnings
from typing import Match, Any, Iterator, cast
+from dataclasses import dataclass
+from collections import UserDict
+import threading
+
+RE_LOCK = threading.Lock()
+SEL_LOCK = threading.RLock()
UNICODE_REPLACEMENT_CHAR = 0xFFFD
@@ -106,7 +112,7 @@
NEWLINE = r'(?:\r\n|(?!\r\n)[\n\f\r])'
WS = fr'(?:[ \t]|{NEWLINE})'
# Comments
-COMMENTS = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
+COMMENTS = r'(?:/\*(?:[^*]|\*(?!/))*\*/)'
# Whitespace with comments included
WSC = fr'(?:{WS}|{COMMENTS})'
# CSS escapes
@@ -114,7 +120,7 @@
CSS_STRING_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}{WS}?|[^\r\n\f]|$|{NEWLINE}))'
# CSS Identifier
IDENTIFIER = fr'''
-(?:(?:-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})+|--)
+(?:(?:--|-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES}))
(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})*)
'''
# `nth` content
@@ -177,8 +183,11 @@
# Whitespace checks
RE_WS = re.compile(WS)
RE_WS_BEGIN = re.compile(fr'^{WSC}*')
-RE_WS_END = re.compile(fr'{WSC}*$')
+RE_WS_END = re.compile(fr'^(?:[ \t]|(?:\n\r|(?!\n\r)[\n\f\r])|{COMMENTS})*')
RE_CUSTOM = re.compile(fr'^{PAT_PSEUDO_CLASS_CUSTOM}$', re.X)
+RE_PSEUDO_CLASS_SPECIAL = re.compile(PAT_PSEUDO_CLASS_SPECIAL, re.I | re.X |
re.U)
+
+QUOTED = ("'", '"')
# Constants
# List split token
@@ -247,7 +256,7 @@
return custom_selectors
-def css_unescape(content: str, string: bool = False) -> str:
+def css_unescape(content: str) -> str:
"""
Unescape CSS value.
@@ -271,7 +280,11 @@
return value
- return (RE_CSS_ESC if not string else RE_CSS_STR_ESC).sub(replace, content)
+ quoted = content.startswith(QUOTED)
+ return (RE_CSS_STR_ESC if quoted else RE_CSS_ESC).sub(
+ replace,
+ content[1:-1] if quoted else content
+ )
def escape(ident: str) -> str:
@@ -309,7 +322,17 @@
"""Initialize."""
self.name = name
- self.re_pattern = re.compile(pattern, re.I | re.X | re.U)
+ self.pattern = pattern
+ self._re_pattern: re.Pattern[str] | None = None
+
+ @property
+ def re_pattern(self) -> re.Pattern[str]:
+ """Retrieve the compiled regular expression pattern."""
+
+ with RE_LOCK:
+ if self._re_pattern is None:
+ self._re_pattern = re.compile(self.pattern, re.I | re.X | re.U)
+ return self._re_pattern
def get_name(self) -> str:
"""Get name."""
@@ -336,7 +359,6 @@
self.patterns[pseudo] = pattern
self.matched_name = None # type: SelectorPattern | None
- self.re_pseudo_name = re.compile(PAT_PSEUDO_CLASS_SPECIAL, re.I | re.X
| re.U)
def get_name(self) -> str:
"""Get name."""
@@ -347,7 +369,7 @@
"""Match the selector."""
pseudo = None
- m = self.re_pseudo_name.match(selector, index)
+ m = RE_PSEUDO_CLASS_SPECIAL.match(selector, index)
if m:
name = util.lower(css_unescape(m.group('name')))
pattern = self.patterns.get(name)
@@ -427,10 +449,187 @@
__repr__ = __str__
+@dataclass
+class CSSPattern:
+ """A CSS pattern that hasn't been processed by `CSSParser` yet."""
+
+ selector: str
+ flags: int
+
+
+class PseudoSelectorMap(UserDict[str, CSSPattern | ct.SelectorList]):
+ """Pseudo selector map."""
+
+ def __setitem__(self, key: str, value: CSSPattern | ct.SelectorList) ->
None:
+ """Set item."""
+
+ self.data[key] = value
+
+ def __getitem__(self, key: str) -> ct.SelectorList:
+ """Get item."""
+
+ with SEL_LOCK:
+ value = self.data[key]
+ if isinstance(value, CSSPattern):
+ value =
CSSParser(value.selector).process_selectors(flags=value.flags)
+ self.data[key] = value
+
+ return value
+
+
+# CSS pattern for `:link` and `:any-link`
+CSS_LINK = CSSPattern('html|*:is(a, area)[href]', FLG_PSEUDO | FLG_HTML)
+# CSS pattern for `:checked`
+CSS_CHECKED = CSSPattern(
+ '''
+ html|*:is(input[type=checkbox], input[type=radio])[checked],
html|option[selected]
+ ''',
+ FLG_PSEUDO | FLG_HTML
+)
+# CSS pattern for `:default` (must compile CSS_CHECKED first)
+CSS_DEFAULT = CSSPattern(
+ '''
+ :checked,
+
+ /*
+ This pattern must be at the end.
+ Special logic is applied to the last selector.
+ */
+ html|form html|*:is(button, input)[type="submit"]
+ ''',
+ FLG_PSEUDO | FLG_HTML | FLG_DEFAULT
+)
+# CSS pattern for `:indeterminate`
+CSS_INDETERMINATE = CSSPattern(
+ '''
+ html|input[type="checkbox"][indeterminate],
+ html|input[type="radio"]:is(:not([name]), [name=""]):not([checked]),
+ html|progress:not([value]),
+
+ /*
+ This pattern must be at the end.
+ Special logic is applied to the last selector.
+ */
+ html|input[type="radio"][name]:not([name='']):not([checked])
+ ''',
+ FLG_PSEUDO | FLG_HTML | FLG_INDETERMINATE
+)
+# CSS pattern for `:disabled`
+CSS_DISABLED = CSSPattern(
+ '''
+ html|*:is(input:not([type=hidden]), button, select, textarea, fieldset,
optgroup, option, fieldset)[disabled],
+ html|optgroup[disabled] > html|option,
+ html|fieldset[disabled] > html|*:is(input:not([type=hidden]), button,
select, textarea, fieldset),
+ html|fieldset[disabled] >
+ html|*:not(legend:nth-of-type(1)) html|*:is(input:not([type=hidden]),
button, select, textarea, fieldset)
+ ''',
+ FLG_PSEUDO | FLG_HTML
+)
+# CSS pattern for `:enabled`
+CSS_ENABLED = CSSPattern(
+ '''
+ html|*:is(input:not([type=hidden]), button, select, textarea, fieldset,
optgroup, option, fieldset):not(:disabled)
+ ''',
+ FLG_PSEUDO | FLG_HTML
+)
+# CSS pattern for `:required`
+CSS_REQUIRED = CSSPattern('html|*:is(input, textarea, select)[required]',
FLG_PSEUDO | FLG_HTML)
+# CSS pattern for `:optional`
+CSS_OPTIONAL = CSSPattern('html|*:is(input, textarea,
select):not([required])', FLG_PSEUDO | FLG_HTML)
+# CSS pattern for `:placeholder-shown`
+CSS_PLACEHOLDER_SHOWN = CSSPattern(
+ '''
+ html|input:is(
+ :not([type]),
+ [type=""],
+ [type=text],
+ [type=search],
+ [type=url],
+ [type=tel],
+ [type=email],
+ [type=password],
+ [type=number]
+ )[placeholder]:not([placeholder='']):is(:not([value]), [value=""]),
+ html|textarea[placeholder]:not([placeholder=''])
+ ''',
+ FLG_PSEUDO | FLG_HTML | FLG_PLACEHOLDER_SHOWN
+)
+# CSS pattern for `:read-write` (CSS_DISABLED must be compiled first)
+CSS_READ_WRITE = CSSPattern(
+ '''
+ html|*:is(
+ textarea,
+ input:is(
+ :not([type]),
+ [type=""],
+ [type=text],
+ [type=search],
+ [type=url],
+ [type=tel],
+ [type=email],
+ [type=number],
+ [type=password],
+ [type=date],
+ [type=datetime-local],
+ [type=month],
+ [type=time],
+ [type=week]
+ )
+ ):not([readonly], :disabled),
+ html|*:is([contenteditable=""], [contenteditable="true" i])
+ ''',
+ FLG_PSEUDO | FLG_HTML
+)
+# CSS pattern for `:read-only`
+CSS_READ_ONLY = CSSPattern('html|*:not(:read-write)', FLG_PSEUDO | FLG_HTML)
+# CSS pattern for `:in-range`
+CSS_IN_RANGE = CSSPattern(
+ '''
+ html|input:is(
+ [type="date"],
+ [type="month"],
+ [type="week"],
+ [type="time"],
+ [type="datetime-local"],
+ [type="number"],
+ [type="range"]
+ ):is(
+ [min],
+ [max]
+ )
+ ''',
+ FLG_PSEUDO | FLG_HTML | FLG_IN_RANGE
+)
+# CSS pattern for `:out-of-range`
+CSS_OUT_OF_RANGE = CSSPattern(
+ '''
+ html|input:is(
+ [type="date"],
+ [type="month"],
+ [type="week"],
+ [type="time"],
+ [type="datetime-local"],
+ [type="number"],
+ [type="range"]
+ ):is(
+ [min],
+ [max]
+ )
+ ''',
+ FLG_PSEUDO | FLG_HTML | FLG_OUT_OF_RANGE
+)
+# CSS pattern for :open
+CSS_OPEN = CSSPattern('html|*:is(details, dialog)[open]', FLG_PSEUDO |
FLG_HTML)
+# CSS pattern for :muted
+CSS_MUTED = CSSPattern('html|*:is(video, audio)[muted]', FLG_PSEUDO | FLG_HTML)
+# CSS pattern default for `:nth-child` "of S" feature
+CSS_NTH_OF_S_DEFAULT = CSSPattern("*|*", FLG_PSEUDO)
+
+
class CSSParser:
"""Parse CSS selectors."""
- css_tokens = (
+ CSS_TOKENS = (
SelectorPattern("pseudo_close", PAT_PSEUDO_CLOSE),
SpecialPseudoPattern(
(
@@ -458,6 +657,29 @@
SelectorPattern("combine", PAT_COMBINE)
)
+ # Pseudos that expand to selectors
+ PSEUDO_SELECTORS = PseudoSelectorMap(
+ {
+ ':link': CSS_LINK,
+ ':any-link': CSS_LINK,
+ ':checked': CSS_CHECKED,
+ ':default': CSS_DEFAULT,
+ ':indeterminate': CSS_INDETERMINATE,
+ ':disabled': CSS_DISABLED,
+ ':enabled': CSS_ENABLED,
+ ':required': CSS_REQUIRED,
+ ':muted': CSS_MUTED,
+ ':open': CSS_OPEN,
+ ':optional': CSS_OPTIONAL,
+ ':read-only': CSS_READ_ONLY,
+ ':read-write': CSS_READ_WRITE,
+ ':in-range': CSS_IN_RANGE,
+ ':out-of-range': CSS_OUT_OF_RANGE,
+ ':placeholder-shown': CSS_PLACEHOLDER_SHOWN,
+ '<nth-of-s>': CSS_NTH_OF_S_DEFAULT
+ }
+ )
+
def __init__(
self,
selector: str,
@@ -472,9 +694,10 @@
self.custom = {} if custom is None else custom
self.count = 0
- def check_count(self) -> None:
+ def increment_count(self, increment: int = 1) -> None:
"""Check the current selector count."""
+ self.count += increment
if self.count > SELECTOR_LIMIT:
raise ValueError(f'Selector exceeds pseudo-class nesting limit of
{SELECTOR_LIMIT}')
@@ -499,28 +722,30 @@
flags = re.DOTALL
if op:
- if m.group('value').startswith(('"', "'")):
- value = css_unescape(m.group('value')[1:-1], True)
- else:
- value = css_unescape(m.group('value'))
+ value = css_unescape(m.group('value'))
if not op:
# Attribute name
pattern = None
elif op.startswith('^'):
# Value start with
- pattern = re.compile(r'^%s.*' % re.escape(value), flags)
+ # `^=` should match nothing if the value is empty, so use `(?!)`
which cannot be matched.
+ value = r'(?!)' if not value else re.escape(value)
+ pattern = re.compile(r'^%s.*' % value, flags)
elif op.startswith('$'):
# Value ends with
- pattern = re.compile(r'.*?%s$' % re.escape(value), flags)
+ # `$=` should match nothing if the value is empty, so use `(?!)`
which cannot be matched.
+ value = r'(?!)' if not value else re.escape(value)
+ pattern = re.compile(r'.*?%s$' % value, flags)
elif op.startswith('*'):
# Value contains
- pattern = re.compile(r'.*?%s.*' % re.escape(value), flags)
+ # `*=` should match nothing if the value is empty, so use `(?!)`
which cannot be matched.
+ value = r'(?!)' if not value else re.escape(value)
+ pattern = re.compile(r'.*?%s.*' % value, flags)
elif op.startswith('~'):
# Value contains word within space separated list
- # `~=` should match nothing if it is empty or contains whitespace,
- # so if either of these cases is present, use `[^\s\S]` which
cannot be matched.
- value = r'[^\s\S]' if not value or RE_WS.search(value) else
re.escape(value)
+ # `*~` should match nothing if the value is empty, so use `(?!)`
which cannot be matched.
+ value = r'(?!)' if not value or RE_WS.search(value) else
re.escape(value)
pattern = re.compile(r'.*?(?:(?<=^)|(?<=[ \t\r\n\f]))%s(?=(?:[
\t\r\n\f]|$)).*' % value, flags)
elif op.startswith('|'):
# Value starts with word in dash separated list
@@ -581,9 +806,7 @@
).process_selectors(flags=FLG_PSEUDO)
self.custom[pseudo] = selector
- self.count += selector.count
- self.check_count()
-
+ self.increment_count(selector.count)
sel.selectors.append(selector)
has_selector = True
return has_selector
@@ -614,66 +837,10 @@
sel.flags |= ct.SEL_SCOPE
elif pseudo == ':empty':
sel.flags |= ct.SEL_EMPTY
- elif pseudo in (':link', ':any-link'):
- self.count += CSS_LINK.count
- self.check_count()
- sel.selectors.append(CSS_LINK)
- elif pseudo == ':checked':
- self.count += CSS_CHECKED.count
- self.check_count()
- sel.selectors.append(CSS_CHECKED)
- elif pseudo == ':default':
- self.count += CSS_DEFAULT.count
- self.check_count()
- sel.selectors.append(CSS_DEFAULT)
- elif pseudo == ':indeterminate':
- self.count += CSS_INDETERMINATE.count
- self.check_count()
- sel.selectors.append(CSS_INDETERMINATE)
- elif pseudo == ":disabled":
- self.count += CSS_DISABLED.count
- self.check_count()
- sel.selectors.append(CSS_DISABLED)
- elif pseudo == ":enabled":
- self.count += CSS_ENABLED.count
- self.check_count()
- sel.selectors.append(CSS_ENABLED)
- elif pseudo == ":required":
- self.count += CSS_REQUIRED.count
- self.check_count()
- sel.selectors.append(CSS_REQUIRED)
- elif pseudo == ":muted":
- self.count += CSS_MUTED.count
- self.check_count()
- sel.selectors.append(CSS_MUTED)
- elif pseudo == ":open":
- self.count += CSS_OPEN.count
- self.check_count()
- sel.selectors.append(CSS_OPEN)
- elif pseudo == ":optional":
- self.count += CSS_OPTIONAL.count
- self.check_count()
- sel.selectors.append(CSS_OPTIONAL)
- elif pseudo == ":read-only":
- self.count += CSS_READ_ONLY.count
- self.check_count()
- sel.selectors.append(CSS_READ_ONLY)
- elif pseudo == ":read-write":
- self.count += CSS_READ_WRITE.count
- self.check_count()
- sel.selectors.append(CSS_READ_WRITE)
- elif pseudo == ":in-range":
- self.count += CSS_IN_RANGE.count
- self.check_count()
- sel.selectors.append(CSS_IN_RANGE)
- elif pseudo == ":out-of-range":
- self.count += CSS_OUT_OF_RANGE.count
- self.check_count()
- sel.selectors.append(CSS_OUT_OF_RANGE)
- elif pseudo == ":placeholder-shown":
- self.count += CSS_PLACEHOLDER_SHOWN.count
- self.check_count()
- sel.selectors.append(CSS_PLACEHOLDER_SHOWN)
+ elif pseudo in self.PSEUDO_SELECTORS:
+ pseudo_selector = self.PSEUDO_SELECTORS[pseudo]
+ self.increment_count(pseudo_selector.count)
+ sel.selectors.append(pseudo_selector)
elif pseudo == ':first-child':
sel.nth.append(ct.SelectorNth(1, False, 0, False, False,
ct.SelectorList()))
elif pseudo == ':last-child':
@@ -772,9 +939,8 @@
nth_sel = self.parse_selectors(iselector, m.end(0), FLG_PSEUDO
| FLG_OPEN)
else:
# Use default `*|*` for `of S`.
- nth_sel = CSS_NTH_OF_S_DEFAULT
- self.count += nth_sel.count
- self.check_count()
+ nth_sel = self.PSEUDO_SELECTORS['<nth-of-s>']
+ self.increment_count(nth_sel.count)
if pseudo_sel == ':nth-child':
sel.nth.append(ct.SelectorNth(s1, var, s2, False, False,
nth_sel))
elif pseudo_sel == ':nth-last-child':
@@ -825,6 +991,12 @@
if not combinator:
combinator = WS_COMBINATOR
if combinator == COMMA_COMBINATOR:
+ if not has_selector:
+ raise SelectorSyntaxError(
+ f"The combinator '{combinator}' at position {index}, must
have a selector before it",
+ self.pattern,
+ index
+ )
sel.rel_type = rel_type
selectors[-1].relations.append(sel)
rel_type = ":" + WS_COMBINATOR
@@ -879,9 +1051,8 @@
# If we are in a forgiving pseudo class, just make the selector a
"no match"
if combinator == COMMA_COMBINATOR:
- sel.no_match = True
+ self.increment_count()
del relations[:]
- selectors.append(sel)
else:
if combinator == COMMA_COMBINATOR:
if not sel.tag and not is_pseudo:
@@ -922,17 +1093,11 @@
FutureWarning
)
contains_own = pseudo == ":-soup-contains-own"
- values = css_unescape(m.group('values'))
- patterns = []
- for token in RE_VALUES.finditer(values):
- if token.group('split'):
- continue
- value = token.group('value')
- if value.startswith(("'", '"')):
- value = css_unescape(value[1:-1], True)
- else:
- value = css_unescape(value)
- patterns.append(value)
+ patterns = [
+ css_unescape(token.group('value'))
+ for token in RE_VALUES.finditer(m.group('values'))
+ if not token.group('split')
+ ]
sel.contains.append(ct.SelectorContains(patterns, contains_own))
has_selector = True
return has_selector
@@ -940,19 +1105,11 @@
def parse_pseudo_lang(self, sel: _Selector, m: Match[str], has_selector:
bool) -> bool:
"""Parse pseudo language."""
- values = m.group('values')
- patterns = []
- for token in RE_VALUES.finditer(values):
- if token.group('split'):
- continue
- value = token.group('value')
- if value.startswith(('"', "'")):
- value = css_unescape(value[1:-1], True)
- else:
- value = css_unescape(value)
-
- patterns.append(value)
-
+ patterns = [
+ css_unescape(token.group('value'))
+ for token in RE_VALUES.finditer(m.group('values'))
+ if not token.group('split')
+ ]
sel.lang.append(ct.SelectorLang(patterns))
has_selector = True
@@ -1030,8 +1187,7 @@
key, m = next(iselector)
if key not in ('combine', 'pseudo_close'):
- self.count += 1
- self.check_count()
+ self.increment_count()
# Handle parts
if key == "at_rule":
@@ -1123,6 +1279,7 @@
# Forgive empty slots in pseudo-classes that have lists (and are
forgiving)
elif is_forgive and (not selectors or not relations):
# Handle normal pseudo-classes with empty slots like `:is()` etc.
+ self.increment_count()
sel.no_match = True
del relations[:]
selectors.append(sel)
@@ -1160,14 +1317,15 @@
# Ignore whitespace and comments at start and end of pattern
m = RE_WS_BEGIN.search(pattern)
index = m.end(0) if m else 0
- m = RE_WS_END.search(pattern)
- end = (m.start(0) - 1) if m else (len(pattern) - 1)
+ m = RE_WS_END.search(pattern[::-1])
+ offset = m.end(0) if m else 0
+ end = len(pattern) - (1 + offset)
if self.debug: # pragma: no cover
print(f'## PARSING: {pattern!r}')
while index <= end:
m = None
- for v in self.css_tokens:
+ for v in self.CSS_TOKENS:
m = v.match(pattern, index, self.flags)
if m:
name = v.get_name()
@@ -1199,169 +1357,3 @@
"""Process selectors."""
return self.parse_selectors(self.selector_iter(self.pattern), index,
flags)
-
-
-# Precompile CSS selector lists for pseudo-classes (additional logic may be
required beyond the pattern)
-# A few patterns are order dependent as they use patterns previous compiled.
-
-# CSS pattern for `:link` and `:any-link`
-CSS_LINK = CSSParser(
- 'html|*:is(a, area)[href]'
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
-# CSS pattern for `:checked`
-CSS_CHECKED = CSSParser(
- '''
- html|*:is(input[type=checkbox], input[type=radio])[checked],
html|option[selected]
- '''
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
-# CSS pattern for `:default` (must compile CSS_CHECKED first)
-CSS_DEFAULT = CSSParser(
- '''
- :checked,
-
- /*
- This pattern must be at the end.
- Special logic is applied to the last selector.
- */
- html|form html|*:is(button, input)[type="submit"]
- '''
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML | FLG_DEFAULT)
-# CSS pattern for `:indeterminate`
-CSS_INDETERMINATE = CSSParser(
- '''
- html|input[type="checkbox"][indeterminate],
- html|input[type="radio"]:is(:not([name]), [name=""]):not([checked]),
- html|progress:not([value]),
-
- /*
- This pattern must be at the end.
- Special logic is applied to the last selector.
- */
- html|input[type="radio"][name]:not([name='']):not([checked])
- '''
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML | FLG_INDETERMINATE)
-# CSS pattern for `:disabled`
-CSS_DISABLED = CSSParser(
- '''
- html|*:is(input:not([type=hidden]), button, select, textarea, fieldset,
optgroup, option, fieldset)[disabled],
- html|optgroup[disabled] > html|option,
- html|fieldset[disabled] > html|*:is(input:not([type=hidden]), button,
select, textarea, fieldset),
- html|fieldset[disabled] >
- html|*:not(legend:nth-of-type(1)) html|*:is(input:not([type=hidden]),
button, select, textarea, fieldset)
- '''
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
-# CSS pattern for `:enabled`
-CSS_ENABLED = CSSParser(
- '''
- html|*:is(input:not([type=hidden]), button, select, textarea, fieldset,
optgroup, option, fieldset):not(:disabled)
- '''
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
-# CSS pattern for `:required`
-CSS_REQUIRED = CSSParser(
- 'html|*:is(input, textarea, select)[required]'
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
-# CSS pattern for `:optional`
-CSS_OPTIONAL = CSSParser(
- 'html|*:is(input, textarea, select):not([required])'
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
-# CSS pattern for `:placeholder-shown`
-CSS_PLACEHOLDER_SHOWN = CSSParser(
- '''
- html|input:is(
- :not([type]),
- [type=""],
- [type=text],
- [type=search],
- [type=url],
- [type=tel],
- [type=email],
- [type=password],
- [type=number]
- )[placeholder]:not([placeholder='']):is(:not([value]), [value=""]),
- html|textarea[placeholder]:not([placeholder=''])
- '''
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML | FLG_PLACEHOLDER_SHOWN)
-# CSS pattern default for `:nth-child` "of S" feature
-CSS_NTH_OF_S_DEFAULT = CSSParser(
- '*|*'
-).process_selectors(flags=FLG_PSEUDO)
-# CSS pattern for `:read-write` (CSS_DISABLED must be compiled first)
-CSS_READ_WRITE = CSSParser(
- '''
- html|*:is(
- textarea,
- input:is(
- :not([type]),
- [type=""],
- [type=text],
- [type=search],
- [type=url],
- [type=tel],
- [type=email],
- [type=number],
- [type=password],
- [type=date],
- [type=datetime-local],
- [type=month],
- [type=time],
- [type=week]
- )
- ):not([readonly], :disabled),
- html|*:is([contenteditable=""], [contenteditable="true" i])
- '''
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
-# CSS pattern for `:read-only`
-CSS_READ_ONLY = CSSParser(
- '''
- html|*:not(:read-write)
- '''
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
-# CSS pattern for `:in-range`
-CSS_IN_RANGE = CSSParser(
- '''
- html|input:is(
- [type="date"],
- [type="month"],
- [type="week"],
- [type="time"],
- [type="datetime-local"],
- [type="number"],
- [type="range"]
- ):is(
- [min],
- [max]
- )
- '''
-).process_selectors(flags=FLG_PSEUDO | FLG_IN_RANGE | FLG_HTML)
-# CSS pattern for `:out-of-range`
-CSS_OUT_OF_RANGE = CSSParser(
- '''
- html|input:is(
- [type="date"],
- [type="month"],
- [type="week"],
- [type="time"],
- [type="datetime-local"],
- [type="number"],
- [type="range"]
- ):is(
- [min],
- [max]
- )
- '''
-).process_selectors(flags=FLG_PSEUDO | FLG_OUT_OF_RANGE | FLG_HTML)
-
-# CSS pattern for :open
-CSS_OPEN = CSSParser(
- '''
- html|*:is(details, dialog)[open]
- '''
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
-
-
-# CSS pattern for :muted
-CSS_MUTED = CSSParser(
- '''
- html|*:is(video, audio)[muted]
- '''
-).process_selectors(flags=FLG_PSEUDO | FLG_HTML)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/tests/test_api.py
new/soupsieve-2.9.2/tests/test_api.py
--- old/soupsieve-2.8.4/tests/test_api.py 2020-02-02 01:00:00.000000000
+0100
+++ new/soupsieve-2.9.2/tests/test_api.py 2020-02-02 01:00:00.000000000
+0100
@@ -603,6 +603,22 @@
with self.assertRaises(ValueError):
sv.compile(selector)
+ def test_excessive_group_selectors(self):
+ """Test excessive selectors in `:is()` and `:where()`."""
+
+ count = 10000
+ selector = f':is({"," * count})'
+
+ # Compile the selector
+ with self.assertRaises(ValueError):
+ sv.compile(selector)
+
+ selector = f':where({"," * count})'
+
+ # Compile the selector
+ with self.assertRaises(ValueError):
+ sv.compile(selector)
+
def test_excessive_custom_selectors(self):
"""Test excessive custom selectors."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/tests/test_level3/test_attribute.py
new/soupsieve-2.9.2/tests/test_level3/test_attribute.py
--- old/soupsieve-2.8.4/tests/test_level3/test_attribute.py 2020-02-02
01:00:00.000000000 +0100
+++ new/soupsieve-2.9.2/tests/test_level3/test_attribute.py 2020-02-02
01:00:00.000000000 +0100
@@ -61,6 +61,39 @@
flags=util.HTML
)
+ def test_attribute_begins_cannot_have_empty(self):
+ """Test attribute `^=` will match nothing when value is empty."""
+
+ # An empty value cannot match anything per the spec.
+ self.assert_selector(
+ self.MARKUP,
+ '[class^=""]',
+ [],
+ flags=util.HTML
+ )
+
+ def test_attribute_end_cannot_have_empty(self):
+ """Test attribute `$=` will match nothing when value is empty."""
+
+ # An empty value cannot match anything per the spec.
+ self.assert_selector(
+ self.MARKUP,
+ '[class$=""]',
+ [],
+ flags=util.HTML
+ )
+
+ def test_attribute_contains_cannot_have_empty(self):
+ """Test attribute `*=` will match nothing when value is empty."""
+
+ # An empty value cannot match anything per the spec.
+ self.assert_selector(
+ self.MARKUP,
+ '[class*=""]',
+ [],
+ flags=util.HTML
+ )
+
def test_attribute_contains_with_newlines(self):
"""Test attribute `*=` will match with new lines."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/tests/test_level3/test_nth_child.py
new/soupsieve-2.9.2/tests/test_level3/test_nth_child.py
--- old/soupsieve-2.8.4/tests/test_level3/test_nth_child.py 2020-02-02
01:00:00.000000000 +0100
+++ new/soupsieve-2.9.2/tests/test_level3/test_nth_child.py 2020-02-02
01:00:00.000000000 +0100
@@ -244,3 +244,30 @@
"""Test that pseudo class fails with bad parameters (basically it
doesn't match)."""
self.assert_raises(':nth-child(a)', SelectorSyntaxError)
+
+ def test_nth_child_an_plus_b_boundaries(self):
+ """Test `An+B` forms whose sequence steps onto index 0 or the last
child."""
+
+ markup = """
+ <body>
+ <i id="0"></i>
+ <i id="1"></i>
+ <i id="2"></i>
+ <i id="3"></i>
+ <i id="4"></i>
+ </body>
+ """
+
+ # `B < 1` sequences whose first valid index is > 1 or is all indices
+ self.assert_selector(markup, "i:nth-child(2n-2)", ['1', '3'],
flags=util.HTML)
+ self.assert_selector(
+ markup, "i:nth-child(n-1)", ['0', '1', '2', '3', '4'],
flags=util.HTML
+ )
+ # `B` equal to the child count (index lands on the last child)
+ self.assert_selector(markup, "i:nth-child(n+5)", ['4'],
flags=util.HTML)
+ self.assert_selector(markup, "i:nth-child(2n+5)", ['4'],
flags=util.HTML)
+ # same boundaries, counted from the end
+ self.assert_selector(
+ markup, "i:nth-last-child(2n-2)", ['1', '3'], flags=util.HTML
+ )
+ self.assert_selector(markup, "i:nth-last-child(n+5)", ['0'],
flags=util.HTML)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/tests/test_level4/test_has.py
new/soupsieve-2.9.2/tests/test_level4/test_has.py
--- old/soupsieve-2.8.4/tests/test_level4/test_has.py 2020-02-02
01:00:00.000000000 +0100
+++ new/soupsieve-2.9.2/tests/test_level4/test_has.py 2020-02-02
01:00:00.000000000 +0100
@@ -140,9 +140,12 @@
)
def test_has_empty(self):
- """Test has with empty slot due to multiple commas."""
+ """Test has with empty slot due to no selectors."""
self.assert_raises('div:has()', SelectorSyntaxError)
+ self.assert_raises('div:has(,a)', SelectorSyntaxError)
+ self.assert_raises('div:has(a,)', SelectorSyntaxError)
+ self.assert_raises('div:has(a,,a)', SelectorSyntaxError)
def test_invalid_incomplete_has(self):
"""Test `:has()` fails with just a combinator."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/soupsieve-2.8.4/tests/test_performance.py
new/soupsieve-2.9.2/tests/test_performance.py
--- old/soupsieve-2.8.4/tests/test_performance.py 1970-01-01
01:00:00.000000000 +0100
+++ new/soupsieve-2.9.2/tests/test_performance.py 2020-02-02
01:00:00.000000000 +0100
@@ -0,0 +1,61 @@
+"""Test performance cases."""
+import unittest
+import sys
+import signal
+import time
+import soupsieve as sv
+from soupsieve.util import SelectorSyntaxError
+
+
+class Timeout(Exception):
+ """Timeout exception."""
+
+
[email protected](not sys.platform.startswith('win'), "Unix/Linux test")
+class TestPerformance(unittest.TestCase):
+ """Test performance."""
+
+ def assert_performance(self, selector):
+ """Test performance of of specific cases."""
+
+ LIMIT = 5.0
+
+ signal.signal(signal.SIGALRM, lambda *_: (_ for _ in
()).throw(Timeout()))
+ signal.setitimer(signal.ITIMER_REAL, LIMIT)
+ dt = None
+ t = 0
+ try:
+ t = time.perf_counter()
+ sv.compile(selector)
+ dt = time.perf_counter() - t
+ except SelectorSyntaxError:
+ dt = time.perf_counter() - t
+ except Timeout:
+ pass
+ finally:
+ signal.setitimer(signal.ITIMER_REAL, 0)
+
+ hung = dt is None
+ success = not hung and dt < 1.0
+
+ if not success:
+ print('Selector:', selector)
+ print(f"{'> %.0f s (HANG)' % LIMIT if hung else '%.3f s' % dt}")
+
+ self.assertTrue(success)
+
+ def test_performance_caase(self):
+ """Test performance cases."""
+
+ for n in (1000, 2000, 4000, 8000):
+ self.assert_performance("[a=" + "a" * n)
+
+ for n in (2000, 4000, 8000, 16000):
+ self.assert_performance("a" * n + "!")
+
+ self.assert_performance("[a=" + "a" * 12000)
+
+ for n in (2000, 4000, 8000, 16000):
+ self.assert_performance("a" + " " * n + "b")
+
+ self.assert_performance("a" + " " * 20000 + "b")