Skip to content

Commit 5b5b94d

Browse files
authored
Merge pull request #365 from AdrianAtZyte/codspeed-benchmarks
Add a CodSpeed-tracked benchmark suite for Selector
2 parents 0e3a030 + 7560cd7 commit 5b5b94d

3 files changed

Lines changed: 182 additions & 1 deletion

File tree

.github/workflows/codspeed.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: CodSpeed
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
paths:
9+
- parsel/**
10+
- tests/benchmarks/**
11+
- .github/workflows/codspeed.yml
12+
- tox.ini
13+
workflow_dispatch:
14+
15+
concurrency:
16+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
17+
cancel-in-progress: true
18+
19+
permissions: {}
20+
21+
jobs:
22+
benchmark:
23+
runs-on: ubuntu-latest
24+
permissions:
25+
contents: read
26+
id-token: write # OIDC authentication with CodSpeed
27+
28+
steps:
29+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
30+
with:
31+
persist-credentials: false
32+
33+
- name: Set up Python 3.13
34+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
35+
with:
36+
python-version: "3.13"
37+
38+
- name: Install dependencies
39+
run: |
40+
pip install tox
41+
tox -n -e benchmark
42+
43+
- name: Run benchmarks
44+
uses: CodSpeedHQ/action@4296e51e7041e24dadb86d1d6e8b9320d223dbe8 # v5.0.3
45+
with:
46+
mode: simulation
47+
run: tox -e benchmark
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
import pytest
6+
7+
from parsel import Selector
8+
9+
if TYPE_CHECKING:
10+
from pytest_codspeed import BenchmarkFixture # type: ignore[import-not-found]
11+
12+
pytest.importorskip("pytest_codspeed", reason="Benchmarks require pytest-codspeed")
13+
14+
ITEM_COUNT = 3000
15+
16+
17+
def _item_html(index: int) -> str:
18+
price = 9.99 + (index % 500)
19+
return (
20+
f'<li class="product" data-index="{index}">'
21+
f'<h2 class="title"><a href="/item/{index}">Product {index}</a></h2>'
22+
f'<span class="price">${price:.2f}</span>'
23+
f'<p class="desc">A description of product {index}.</p>'
24+
"</li>"
25+
)
26+
27+
28+
def _build_catalog(item_count: int) -> str:
29+
"""Return an HTML catalog page listing *item_count* products."""
30+
items = "".join(_item_html(index) for index in range(item_count))
31+
return (
32+
"<!DOCTYPE html><html><head><title>Catalog</title></head><body>"
33+
f'<ul class="catalog">{items}</ul>'
34+
"</body></html>"
35+
)
36+
37+
38+
CATALOG_HTML = _build_catalog(ITEM_COUNT)
39+
40+
41+
def test_parse(benchmark: BenchmarkFixture) -> None:
42+
benchmark(lambda: Selector(text=CATALOG_HTML))
43+
44+
45+
def test_query_broad_css(benchmark: BenchmarkFixture) -> None:
46+
sel = Selector(text=CATALOG_HTML)
47+
48+
def run() -> None:
49+
assert len(sel.css(".product")) == ITEM_COUNT
50+
51+
benchmark(run)
52+
53+
54+
def test_query_broad_xpath(benchmark: BenchmarkFixture) -> None:
55+
sel = Selector(text=CATALOG_HTML)
56+
57+
def run() -> None:
58+
assert len(sel.xpath("//*[@class='product']")) == ITEM_COUNT
59+
60+
benchmark(run)
61+
62+
63+
def test_query_chained_elements_css(benchmark: BenchmarkFixture) -> None:
64+
"""The ``for item in sel.css(...): item.css(...)`` spider pattern."""
65+
items = Selector(text=CATALOG_HTML).css(".product")
66+
67+
def run() -> None:
68+
for item in items:
69+
item.css("h2.title a")
70+
item.css(".price")
71+
item.css(".desc")
72+
73+
benchmark(run)
74+
75+
76+
def test_query_chained_elements_xpath(benchmark: BenchmarkFixture) -> None:
77+
items = Selector(text=CATALOG_HTML).xpath("//*[@class='product']")
78+
79+
def run() -> None:
80+
for item in items:
81+
item.xpath(".//h2[@class='title']/a")
82+
item.xpath(".//*[@class='price']")
83+
item.xpath(".//*[@class='desc']")
84+
85+
benchmark(run)
86+
87+
88+
def test_query_chained_values_css(benchmark: BenchmarkFixture) -> None:
89+
"""Extracting title, link, price and description text/attrs per item."""
90+
items = Selector(text=CATALOG_HTML).css(".product")
91+
92+
def run() -> None:
93+
for item in items:
94+
item.css("h2.title a::text").get()
95+
item.css("h2.title a::attr(href)").get()
96+
item.css(".price::text").get()
97+
item.css(".desc::text").get()
98+
99+
benchmark(run)
100+
101+
102+
def test_query_chained_values_xpath(benchmark: BenchmarkFixture) -> None:
103+
items = Selector(text=CATALOG_HTML).xpath("//*[@class='product']")
104+
105+
def run() -> None:
106+
for item in items:
107+
item.xpath(".//h2[@class='title']/a/text()").get()
108+
item.xpath(".//h2[@class='title']/a/@href").get()
109+
item.xpath(".//*[@class='price']/text()").get()
110+
item.xpath(".//*[@class='desc']/text()").get()
111+
112+
benchmark(run)
113+
114+
115+
def test_re(benchmark: BenchmarkFixture) -> None:
116+
prices = Selector(text=CATALOG_HTML).css(".price::text")
117+
benchmark(lambda: prices.re(r"[\d.]+"))
118+
119+
120+
def test_re_first(benchmark: BenchmarkFixture) -> None:
121+
prices = Selector(text=CATALOG_HTML).css(".price::text")
122+
benchmark(lambda: prices.re_first(r"[\d.]+"))

tox.ini

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = typing,pylint,docs,twinecheck,pre-commit,py310,py311,py312,py313,py314,py315,pypy3.11
2+
envlist = typing,pylint,docs,twinecheck,pre-commit,py310,py311,py312,py313,py314,py315,pypy3.11,benchmark
33

44
[testenv]
55
usedevelop = True
@@ -56,6 +56,18 @@ deps = pre-commit
5656
commands = pre-commit run --all-files --show-diff-on-failure
5757
skip_install = true
5858

59+
# CPU benchmarks, tracked on CodSpeed.
60+
[testenv:benchmark]
61+
basepython = python3.13
62+
deps =
63+
{[testenv]deps}
64+
pytest-codspeed
65+
passenv =
66+
*codspeed*
67+
*ci*
68+
commands =
69+
pytest {posargs:tests/benchmarks} --codspeed --codspeed-mode=simulation
70+
5971
[testenv:min-deps]
6072
description = Test with pinned minimum dependency versions
6173
basepython = python3.10

0 commit comments

Comments
 (0)