Skip to content

Commit 0c4bce2

Browse files
authored
Add non-Linux CI jobs. (#154)
1 parent e99c506 commit 0c4bce2

File tree

7 files changed

+89
-31
lines changed

7 files changed

+89
-31
lines changed

.github/workflows/tests-macos.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: macOS
2+
on: [push, pull_request]
3+
4+
jobs:
5+
tests:
6+
runs-on: macos-latest
7+
strategy:
8+
fail-fast: false
9+
matrix:
10+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Python ${{ matrix.python-version }}
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: ${{ matrix.python-version }}
19+
20+
- name: Run tests
21+
run: |
22+
pip install -U pip
23+
pip install -U tox
24+
tox -e py
25+
26+
- name: Upload coverage report
27+
uses: codecov/codecov-action@v5

.github/workflows/tests.yml renamed to .github/workflows/tests-ubuntu.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Tests
1+
name: Ubuntu
22
on: [push, pull_request]
33

44
jobs:
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Windows
2+
on: [push, pull_request]
3+
4+
jobs:
5+
tests:
6+
runs-on: windows-latest
7+
strategy:
8+
fail-fast: false
9+
matrix:
10+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Python ${{ matrix.python-version }}
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: ${{ matrix.python-version }}
19+
20+
- name: Run tests
21+
run: |
22+
pip install -U pip
23+
pip install -U tox
24+
tox -e py
25+
26+
- name: Upload coverage report
27+
uses: codecov/codecov-action@v5

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/astral-sh/ruff-pre-commit
3-
rev: v0.9.10
3+
rev: v0.11.2
44
hooks:
55
- id: ruff
66
args: [ --fix ]

cssselect/parser.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ def parse_selector(stream: TokenStream) -> tuple[Tree, PseudoElement | None]:
562562
)
563563
if peek.is_delim("+", ">", "~"):
564564
# A combinator
565-
combinator = cast(str, stream.next().value)
565+
combinator = cast("str", stream.next().value)
566566
stream.skip_whitespace()
567567
else:
568568
# By exclusion, the last parse_simple_selector() ended
@@ -608,7 +608,7 @@ def parse_simple_selector(
608608
f"Got pseudo-element ::{pseudo_element} not at the end of a selector"
609609
)
610610
if peek.type == "HASH":
611-
result = Hash(result, cast(str, stream.next().value))
611+
result = Hash(result, cast("str", stream.next().value))
612612
elif peek == ("DELIM", "."):
613613
stream.next()
614614
result = Class(result, stream.next_ident())
@@ -720,7 +720,7 @@ def parse_relative_selector(stream: TokenStream) -> tuple[Token, Selector]:
720720
("DELIM", "."),
721721
("DELIM", "*"),
722722
]:
723-
subselector += cast(str, next.value)
723+
subselector += cast("str", next.value)
724724
elif next == ("DELIM", ")"):
725725
result = parse(subselector)
726726
return combinator, result[0]
@@ -774,13 +774,13 @@ def parse_attrib(selector: Tree, stream: TokenStream) -> Attrib:
774774
stream.skip_whitespace()
775775
next = stream.next()
776776
if next == ("DELIM", "]"):
777-
return Attrib(selector, namespace, cast(str, attrib), "exists", None)
777+
return Attrib(selector, namespace, cast("str", attrib), "exists", None)
778778
if next == ("DELIM", "="):
779779
op = "="
780780
elif next.is_delim("^", "$", "*", "~", "|", "!") and (
781781
stream.peek() == ("DELIM", "=")
782782
):
783-
op = cast(str, next.value) + "="
783+
op = cast("str", next.value) + "="
784784
stream.next()
785785
else:
786786
raise SelectorSyntaxError(f"Operator expected, got {next}")
@@ -792,7 +792,7 @@ def parse_attrib(selector: Tree, stream: TokenStream) -> Attrib:
792792
next = stream.next()
793793
if next != ("DELIM", "]"):
794794
raise SelectorSyntaxError(f"Expected ']', got {next}")
795-
return Attrib(selector, namespace, cast(str, attrib), op, value)
795+
return Attrib(selector, namespace, cast("str", attrib), op, value)
796796

797797

798798
def parse_series(tokens: Iterable[Token]) -> tuple[int, int]:
@@ -806,7 +806,7 @@ def parse_series(tokens: Iterable[Token]) -> tuple[int, int]:
806806
for token in tokens:
807807
if token.type == "STRING":
808808
raise ValueError("String tokens not allowed in series.")
809-
s = "".join(cast(str, token.value) for token in tokens).strip()
809+
s = "".join(cast("str", token.value) for token in tokens).strip()
810810
if s == "odd":
811811
return 2, 1
812812
if s == "even":
@@ -867,7 +867,7 @@ def value(self) -> str | None:
867867
def css(self) -> str:
868868
if self.type == "STRING":
869869
return repr(self.value)
870-
return cast(str, self.value)
870+
return cast("str", self.value)
871871

872872

873873
class EOFToken(Token):
@@ -1030,7 +1030,7 @@ def next_ident(self) -> str:
10301030
next = self.next()
10311031
if next.type != "IDENT":
10321032
raise SelectorSyntaxError(f"Expected ident, got {next}")
1033-
return cast(str, next.value)
1033+
return cast("str", next.value)
10341034

10351035
def next_ident_or_star(self) -> str | None:
10361036
next = self.next()

cssselect/xpath.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
from __future__ import annotations
1515

1616
import re
17-
from collections.abc import Callable
18-
from typing import TYPE_CHECKING, Optional, cast
17+
from typing import TYPE_CHECKING, cast
1918

2019
from cssselect.parser import (
2120
Attrib,
@@ -38,6 +37,8 @@
3837
)
3938

4039
if TYPE_CHECKING:
40+
from collections.abc import Callable
41+
4142
# typing.Self requires Python 3.11
4243
from typing_extensions import Self
4344

@@ -289,7 +290,7 @@ def xpath(self, parsed_selector: Tree) -> XPathExpr:
289290
"""Translate any parsed selector object."""
290291
type_name = type(parsed_selector).__name__
291292
method = cast(
292-
Optional[Callable[[Tree], XPathExpr]],
293+
"Callable[[Tree], XPathExpr] | None",
293294
getattr(self, f"xpath_{type_name.lower()}", None),
294295
)
295296
if method is None:
@@ -302,7 +303,7 @@ def xpath_combinedselector(self, combined: CombinedSelector) -> XPathExpr:
302303
"""Translate a combined selector."""
303304
combinator = self.combinator_mapping[combined.combinator]
304305
method = cast(
305-
Callable[[XPathExpr, XPathExpr], XPathExpr],
306+
"Callable[[XPathExpr, XPathExpr], XPathExpr]",
306307
getattr(self, f"xpath_{combinator}_combinator"),
307308
)
308309
return method(self.xpath(combined.selector), self.xpath(combined.subselector))
@@ -321,10 +322,10 @@ def xpath_relation(self, relation: Relation) -> XPathExpr:
321322
subselector = relation.subselector
322323
right = self.xpath(subselector.parsed_tree)
323324
method = cast(
324-
Callable[[XPathExpr, XPathExpr], XPathExpr],
325+
"Callable[[XPathExpr, XPathExpr], XPathExpr]",
325326
getattr(
326327
self,
327-
f"xpath_relation_{self.combinator_mapping[cast(str, combinator.value)]}_combinator",
328+
f"xpath_relation_{self.combinator_mapping[cast('str', combinator.value)]}_combinator",
328329
),
329330
)
330331
return method(xpath, right)
@@ -351,7 +352,7 @@ def xpath_function(self, function: Function) -> XPathExpr:
351352
"""Translate a functional pseudo-class."""
352353
method_name = "xpath_{}_function".format(function.name.replace("-", "_"))
353354
method = cast(
354-
Optional[Callable[[XPathExpr, Function], XPathExpr]],
355+
"Callable[[XPathExpr, Function], XPathExpr] | None",
355356
getattr(self, method_name, None),
356357
)
357358
if not method:
@@ -362,7 +363,8 @@ def xpath_pseudo(self, pseudo: Pseudo) -> XPathExpr:
362363
"""Translate a pseudo-class."""
363364
method_name = "xpath_{}_pseudo".format(pseudo.ident.replace("-", "_"))
364365
method = cast(
365-
Optional[Callable[[XPathExpr], XPathExpr]], getattr(self, method_name, None)
366+
"Callable[[XPathExpr], XPathExpr] | None",
367+
getattr(self, method_name, None),
366368
)
367369
if not method:
368370
# TODO: better error message for pseudo-elements?
@@ -373,7 +375,7 @@ def xpath_attrib(self, selector: Attrib) -> XPathExpr:
373375
"""Translate an attribute selector."""
374376
operator = self.attribute_operator_mapping[selector.operator]
375377
method = cast(
376-
Callable[[XPathExpr, str, Optional[str]], XPathExpr],
378+
"Callable[[XPathExpr, str, str | None], XPathExpr]",
377379
getattr(self, f"xpath_attrib_{operator}"),
378380
)
379381
if self.lower_case_attribute_names:
@@ -391,7 +393,7 @@ def xpath_attrib(self, selector: Attrib) -> XPathExpr:
391393
if selector.value is None:
392394
value = None
393395
elif self.lower_case_attribute_values:
394-
value = cast(str, selector.value.value).lower()
396+
value = cast("str", selector.value.value).lower()
395397
else:
396398
value = selector.value.value
397399
return method(self.xpath(selector.selector), attrib, value)
@@ -645,15 +647,15 @@ def xpath_contains_function(
645647
raise ExpressionError(
646648
f"Expected a single string or ident for :contains(), got {function.arguments!r}"
647649
)
648-
value = cast(str, function.arguments[0].value)
650+
value = cast("str", function.arguments[0].value)
649651
return xpath.add_condition(f"contains(., {self.xpath_literal(value)})")
650652

651653
def xpath_lang_function(self, xpath: XPathExpr, function: Function) -> XPathExpr:
652654
if function.argument_types() not in (["STRING"], ["IDENT"]):
653655
raise ExpressionError(
654656
f"Expected a single string or ident for :lang(), got {function.arguments!r}"
655657
)
656-
value = cast(str, function.arguments[0].value)
658+
value = cast("str", function.arguments[0].value)
657659
return xpath.add_condition(f"lang({self.xpath_literal(value)})")
658660

659661
# Pseudo: dispatch by pseudo-class name

tests/test_cssselect.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,7 @@ def xpath(css: str) -> str:
728728

729729
def operator_id(selector: str) -> list[str]:
730730
xpath = CustomTranslator().css_to_xpath(selector)
731-
items = typing.cast(list["etree._Element"], document.xpath(xpath))
731+
items = typing.cast("list[etree._Element]", document.xpath(xpath))
732732
items.sort(key=sort_key)
733733
return [element.get("id", "nil") for element in items]
734734

@@ -739,7 +739,9 @@ def operator_id(selector: str) -> list[str]:
739739
def test_series(self) -> None:
740740
def series(css: str) -> tuple[int, int] | None:
741741
(selector,) = parse(f":nth-child({css})")
742-
args = typing.cast(FunctionalPseudoElement, selector.parsed_tree).arguments
742+
args = typing.cast(
743+
"FunctionalPseudoElement", selector.parsed_tree
744+
).arguments
743745
try:
744746
return parse_series(args)
745747
except ValueError:
@@ -771,7 +773,7 @@ def test_lang(self) -> None:
771773

772774
def langid(selector: str) -> list[str]:
773775
xpath = css_to_xpath(selector)
774-
items = typing.cast(list["etree._Element"], document.xpath(xpath))
776+
items = typing.cast("list[etree._Element]", document.xpath(xpath))
775777
items.sort(key=sort_key)
776778
return [element.get("id", "nil") for element in items]
777779

@@ -800,7 +802,7 @@ def xpath_pseudo_element(
800802
self, xpath: XPathExpr, pseudo_element: PseudoElement
801803
) -> XPathExpr:
802804
self.argument_types += typing.cast(
803-
FunctionalPseudoElement, pseudo_element
805+
"FunctionalPseudoElement", pseudo_element
804806
).argument_types()
805807
return xpath
806808

@@ -827,11 +829,11 @@ def test_select(self) -> None:
827829

828830
def select_ids(selector: str, html_only: bool) -> list[str]:
829831
xpath = css_to_xpath(selector)
830-
items = typing.cast(list["etree._Element"], document.xpath(xpath))
832+
items = typing.cast("list[etree._Element]", document.xpath(xpath))
831833
if html_only:
832834
assert items == []
833835
xpath = html_css_to_xpath(selector)
834-
items = typing.cast(list["etree._Element"], document.xpath(xpath))
836+
items = typing.cast("list[etree._Element]", document.xpath(xpath))
835837
items.sort(key=sort_key)
836838
return [element.get("id", "nil") for element in items]
837839

@@ -1065,14 +1067,14 @@ def pcss(main: str, *selectors: str, **kwargs: bool) -> list[str]:
10651067

10661068
def test_select_shakespeare(self) -> None:
10671069
document = html.document_fromstring(HTML_SHAKESPEARE)
1068-
body = typing.cast(list["etree._Element"], document.xpath("//body"))[0]
1070+
body = typing.cast("list[etree._Element]", document.xpath("//body"))[0]
10691071
css_to_xpath = GenericTranslator().css_to_xpath
10701072

10711073
basestring_ = (str, bytes)
10721074

10731075
def count(selector: str) -> int:
10741076
xpath = css_to_xpath(selector)
1075-
results = typing.cast(list["etree._Element"], body.xpath(xpath))
1077+
results = typing.cast("list[etree._Element]", body.xpath(xpath))
10761078
assert not isinstance(results, basestring_)
10771079
found = set()
10781080
for item in results:

0 commit comments

Comments
 (0)