Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions cssselect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
============================

This module supports selecting XML/HTML elements based on CSS selectors.
See the `CSSSelector` class for details.


:copyright: (c) 2007-2012 Ian Bicking and contributors.
See AUTHORS for more details.
Expand Down
17 changes: 3 additions & 14 deletions cssselect/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,13 +835,7 @@ def parse_attrib(selector: Tree, stream: TokenStream) -> Attrib:


def parse_series(tokens: Iterable[Token]) -> tuple[int, int]:
"""
Parses the arguments for :nth-child() and friends.

:raises: A list of tokens
:returns: :``(a, b)``

"""
"""Parses the arguments for :nth-child() and friends."""
for token in tokens:
if token.type == "STRING":
raise ValueError("String tokens not allowed in series.")
Expand Down Expand Up @@ -1028,19 +1022,14 @@ def tokenize(s: str) -> Iterator[Token]:

match = _match_ident(s, pos=pos)
if match:
value = _sub_simple_escape(
_replace_simple, _sub_unicode_escape(_replace_unicode, match.group())
)
value = unescape_ident(match.group())
yield Token("IDENT", value, pos)
pos = match.end()
continue

match = _match_hash(s, pos=pos)
if match:
value = _sub_simple_escape(
_replace_simple,
_sub_unicode_escape(_replace_unicode, match.group()[1:]),
)
value = unescape_ident(match.group()[1:])
yield Token("HASH", value, pos)
pos = match.end()
continue
Expand Down
14 changes: 9 additions & 5 deletions cssselect/xpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,14 @@ def _xpath_add_selector_list_condition(
condition = ""
for e in (self.xpath(selector) for selector in selector_list):
if e.path:
# E.g. a :has() argument: it translates to a path, which
# cannot be embedded into a predicate of the outer expression.
# Only a combined selector (e.g. "a b") translates to a path,
# which cannot be embedded into a predicate of the outer
# expression. The parser rejects combinators in these arguments,
# so this is only reachable through a hand-built Matching or
# SpecificityAdjustment node.
raise ExpressionError(
":has() is not supported inside :is() and :where()"
"Combined selectors are not supported inside "
":is(), :where() and :matches()"
)
e.add_name_test()
if not e.condition:
Expand Down Expand Up @@ -601,7 +605,7 @@ def xpath_nth_child_function(
expressions.append(f"{siblings_count} >= {b_min_1}")
else:
# if a<0, and (b-1)<0, no "n" satisfies this,
# this is tested above as an early exist condition
# this is tested above as an early exit condition
# otherwise,
expressions.append(f"{siblings_count} <= {b_min_1}")

Expand All @@ -614,7 +618,7 @@ def xpath_nth_child_function(
# - or:
# count(***-sibling::***) - (b-1) = -n = 0, -1, -2, -3, etc.,
# i.e. count(***-sibling::***) <= (b-1)
# we we just did above.
# we just did above.
#
if abs(a) != 1:
# count(***-sibling::***) - (b-1) ≡ 0 (mod a)
Expand Down
16 changes: 13 additions & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,25 @@ be implemented):

* The ``:scope`` pseudo-class. Limitation: it can only be used at a start of a
selector.
* The ``:is()``, ``:where()`` and ``:has()`` pseudo-classes. Limitation:
``:has()`` cannot contain nested ``:has()`` or ``:not()``.
* The ``:is()`` and ``:where()`` pseudo-classes. Limitation: their arguments
are a comma-separated list of *compound selectors*; combinators are not
allowed (e.g. ``:is(a b)`` or ``:is(a > b)``). ``:not()`` and ``:scope`` are
also rejected inside them, while ``:has()`` is supported (e.g.
``:is(:has(> a))``).
* The ``:has()`` pseudo-class. Limitation: it takes a single argument, made of
an optional leading combinator (``>``, ``+`` or ``~``) followed by one
*compound selector* built only from type, class and universal selectors
(e.g. ``:has(> a.important)``). Anything else is unsupported, e.g. an ID
(``:has(#id)``), or a selector list (``:has(a, b)``).

These are non-standard extensions:

* The ``:contains(text)`` pseudo-class that existed in `an early draft`_
but was then removed.
* The ``!=`` attribute operator. ``[foo!=bar]`` is the same as
``:not([foo=bar])``.
``:not([foo=bar])``, except for an empty value: ``[foo!='']`` matches only
elements that do have a ``foo`` attribute and whose value is not empty,
while ``:not([foo=''])`` also matches elements without a ``foo`` attribute.
* ``:not()`` accepts a *sequence of simple selectors*, not just single
*simple selector*. For example, ``:not(a.important[rel])`` is allowed,
even though the negation contains 3 *simple selectors*.
Expand Down
11 changes: 3 additions & 8 deletions tests/test_cssselect.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,6 @@ def parse_many(first: str, *others: str) -> list[str]:
]
assert parse_many("div > p") == ["CombinedSelector[Element[div] > Element[p]]"]
assert parse_many("td:first") == ["Pseudo[Element[td]:first]"]
assert parse_many("td:first") == ["Pseudo[Element[td]:first]"]
assert parse_many("td :first") == [
"CombinedSelector[Element[td] <followed> Pseudo[Element[*]:first]]"
]
assert parse_many("td :first") == [
"CombinedSelector[Element[td] <followed> Pseudo[Element[*]:first]]"
]
Expand Down Expand Up @@ -892,7 +888,8 @@ class LowerValues(GenericTranslator):
combined = parse("a b")[0].parsed_tree
matching = Matching(base, [combined])
with pytest.raises(
ExpressionError, match=r"not supported inside :is\(\) and :where\(\)"
ExpressionError,
match=r"not supported inside :is\(\), :where\(\) and :matches\(\)",
):
GenericTranslator().xpath(matching)

Expand Down Expand Up @@ -1082,9 +1079,7 @@ def operator_id(selector: str) -> list[str]:
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("Function", selector.parsed_tree).arguments
try:
return parse_series(args)
except ValueError:
Expand Down
Loading