Skip to content

Commit ffdb7a6

Browse files
committed
ci: Add end-to-end workflow
1 parent 978f34e commit ffdb7a6

File tree

5 files changed

+266
-7
lines changed

5 files changed

+266
-7
lines changed

.github/workflows/end-to-end.yml

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: end-to-end
2+
3+
on: [pull_request] # ??
4+
5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.ref }}
7+
cancel-in-progress: true
8+
9+
permissions: {}
10+
11+
defaults:
12+
run:
13+
shell: bash
14+
15+
jobs:
16+
end-to-end:
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: read
20+
steps:
21+
- name: Checkout code
22+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
23+
24+
- name: Setup Python
25+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
26+
id: setup-python
27+
with:
28+
python-version: 3.13
29+
check-latest: True
30+
cache: pip
31+
32+
- name: Restore python-venv
33+
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
34+
id: cache-python-venv
35+
with:
36+
path: venv/
37+
key: >-
38+
python-venv
39+
-ubuntu-latest
40+
-3.13
41+
-${{ hashFiles('ci/requirements/skore/python-3.13/scikit-learn-1.7/test-requirements.txt') }}
42+
-${{ hashFiles('ci/requirements/skore-hub-project/python-3.13/scikit-learn-1.7/test-requirements.txt') }}
43+
-${{ hashFiles('ci/requirements/skore-local-project/python-3.13/scikit-learn-1.7/test-requirements.txt') }}
44+
45+
- name: Setup python-venv
46+
run: |
47+
set -eu
48+
49+
# Ensure venv is created
50+
python -m venv venv
51+
52+
# Activate venv for each step depending on the OS
53+
echo "${GITHUB_WORKSPACE}/venv/bin" >> ${GITHUB_PATH}
54+
echo "VIRTUAL_ENV=${GITHUB_WORKSPACE}/venv" >> ${GITHUB_ENV}
55+
56+
- name: Install dependencies in python-venv
57+
if: steps.cache-python-venv.outputs.cache-hit != 'true'
58+
run: |
59+
python -m pip install --upgrade pip build
60+
61+
for package in 'skore' 'skore-hub-project' 'skore-local-project' ; do
62+
python -m pip install --requirement "ci/requirements/${package}/python-3.13/scikit-learn-1.7/test-requirements.txt"
63+
done
64+
65+
- name: Save python-venv
66+
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
67+
if: steps.cache-python-venv.outputs.cache-hit != 'true'
68+
with:
69+
path: venv/
70+
key: ${{ steps.cache-python-venv.outputs.cache-primary-key }}
71+
72+
- name: Build and install
73+
run: |
74+
for package in 'skore' 'skore-hub-project' 'skore-local-project'; do
75+
(
76+
cd "${package}/"
77+
78+
# build
79+
python -m build
80+
81+
# install
82+
wheel=(dist/*.whl); python -m pip install --force-reinstall --no-deps "${wheel}"
83+
)
84+
done
85+
86+
- name: Test
87+
timeout-minutes: 10
88+
working-directory: tests/end-to-end
89+
run: python -m pytest --import-mode=importlib --no-header --verbosity=2 --dist=loadscope --numprocesses auto --no-cov

.pre-commit-config.yaml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ repos:
1111
- id: trailing-whitespace
1212

1313
- repo: https://github.com/crate-ci/typos
14-
rev: v1.30.2
14+
rev: v1.33.1
1515
hooks:
1616
- id: typos
1717

@@ -22,16 +22,14 @@ repos:
2222
args: [--keep-id]
2323

2424
- repo: https://github.com/astral-sh/ruff-pre-commit
25-
rev: v0.9.10
25+
rev: v0.11.13
2626
hooks:
27-
- id: ruff
28-
files: ^((skore|skore-hub-project|skore-local-project)/(hatch|src|tests))|(examples)/
27+
- id: ruff-check
2928
args: [--fix]
3029
- id: ruff-format
31-
files: ^((skore|skore-hub-project|skore-local-project)/(hatch|src|tests))|(examples)/
3230

3331
- repo: https://github.com/pre-commit/mirrors-mypy
34-
rev: v1.15.0
32+
rev: v1.16.1
3533
hooks:
3634
- id: mypy
3735
files: ^(skore|skore-hub-project|skore-local-project)/src/

ci/tests/end-to-end/conftest.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from pytest import fixture
2+
3+
4+
def pytest_configure(config):
5+
import matplotlib
6+
7+
# Use a non-interactive ``matplotlib.backend`` that can only write to files.
8+
#
9+
# https://github.com/matplotlib/matplotlib/issues/29119
10+
# https://matplotlib.org/stable/users/explain/figure/backends.html#selecting-a-backend
11+
matplotlib.use("agg")
12+
13+
14+
@fixture
15+
def monkeypatch_tmpdir(monkeypatch, tmp_path):
16+
"""
17+
Change ``TMPDIR`` used by ``tempfile.gettempdir()`` to point to ``tmp_path``, so
18+
that it is automatically deleted after use, with no impact on user's environment.
19+
20+
Force the reload of the ``tempfile`` module to change the cached return of
21+
``tempfile.gettempdir()``.
22+
23+
https://docs.python.org/3/library/tempfile.html#tempfile.gettempdir
24+
"""
25+
import importlib
26+
import tempfile
27+
28+
monkeypatch.setenv("TMPDIR", str(tmp_path))
29+
importlib.reload(tempfile)
30+
31+
32+
@fixture
33+
def monkeypatch_skrub(monkeypatch):
34+
"""
35+
Make `skrub.TableReport.html_snippet()` reproducible
36+
37+
https://github.com/skrub-data/skrub/blob/35f573ce586fe61ef2c72f4c0c4b188ebf2e664b/skrub/_reporting/_html.py#L153
38+
"""
39+
monkeypatch.setattr("secrets.token_hex", lambda: "<token>")
40+
41+
42+
@fixture
43+
def monkeypatch_matplotlib(monkeypatch):
44+
"""
45+
Make `matplotlib.Figure.savefig(format="svg")` reproducible
46+
47+
https://matplotlib.org/stable/users/prev_whats_new/whats_new_2.1.0.html#reproducible-ps-pdf-and-svg-output
48+
https://matplotlib.org/stable/users/prev_whats_new/whats_new_3.10.0.html#svg-id-rcparam
49+
"""
50+
import matplotlib
51+
52+
monkeypatch.setenv("SOURCE_DATE_EPOCH", "0")
53+
54+
matplotlib_rcparams = matplotlib.rcParams.copy()
55+
matplotlib.rcParams["svg.hashsalt"] = "<hashsalt>"
56+
57+
if "svg.id" in matplotlib.rcParams:
58+
matplotlib.rcParams["svg.id"] = "<id>"
59+
60+
try:
61+
yield
62+
finally:
63+
matplotlib.rcParams = matplotlib_rcparams
64+
65+
66+
@fixture(autouse=True)
67+
def setup(
68+
monkeypatch_tmpdir,
69+
monkeypatch_matplotlib,
70+
monkeypatch_skrub,
71+
): ...

ci/tests/end-to-end/test_import.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from pytest import fixture
2+
from sklearn.datasets import make_classification, make_regression, fetch_openml
3+
from sklearn.linear_model import LinearRegression, LogisticRegression
4+
from sklearn.model_selection import train_test_split
5+
from skrub import tabular_learner
6+
from sklearn.model_selection import GridSearchCV
7+
8+
9+
@fixture(scope="module")
10+
def regression():
11+
X, y = make_regression(random_state=42)
12+
X_train, X_test, y_train, y_test = train_test_split(
13+
X, y, test_size=0.2, random_state=42
14+
)
15+
16+
return {
17+
"estimator": LinearRegression(),
18+
"X_train": X_train,
19+
"y_train": y_train,
20+
"X_test": X_test,
21+
"y_test": y_test,
22+
}
23+
24+
25+
@fixture(scope="module")
26+
def classification():
27+
X, y = make_classification(n_classes=2, random_state=42)
28+
X_train, X_test, y_train, y_test = train_test_split(
29+
X, y, test_size=0.2, random_state=42
30+
)
31+
32+
return {
33+
"estimator": LogisticRegression(),
34+
"X_train": X_train,
35+
"y_train": y_train,
36+
"X_test": X_test,
37+
"y_test": y_test,
38+
}
39+
40+
41+
@fixture(scope="module")
42+
def gridsearch():
43+
X, y = fetch_openml("adult", version=2, as_frame=True, return_X_y=True)
44+
y = 1 * (y == ">50K")
45+
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
46+
47+
return {
48+
"estimator": GridSearchCV(
49+
estimator=tabular_learner("classification"),
50+
param_grid={
51+
"histgradientboostingclassifier__learning_rate": [0.01, 0.1, 0.2],
52+
"histgradientboostingclassifier__max_depth": [1, 3, 5],
53+
"histgradientboostingclassifier__max_leaf_nodes": [30, 60, 90],
54+
},
55+
cv=5,
56+
n_jobs=-1,
57+
refit=True,
58+
scoring="neg_log_loss",
59+
),
60+
"X_train": X_train,
61+
"y_train": y_train,
62+
"X_test": X_test,
63+
"y_test": y_test,
64+
}
65+
66+
67+
def test_put_with_local_project(tmp_path, regression, classification, gridsearch):
68+
import skore
69+
import skore_local_project
70+
71+
project = skore.Project("<name>", workspace=tmp_path)
72+
73+
assert isinstance(project._Project__project, skore_local_project.Project)
74+
assert project.mode == "local"
75+
assert project.name == "<name>"
76+
assert project._Project__project.workspace == tmp_path
77+
assert project._Project__project.name == "<name>"
78+
79+
project.put("regression", skore.EstimatorReport(**regression))
80+
project.put("classification", skore.EstimatorReport(**classification))
81+
project.put("gridsearch", skore.EstimatorReport(**gridsearch))
82+
83+
84+
def test_simili_put_with_hub_project(regression, classification, gridsearch):
85+
import skore
86+
import skore_hub_project
87+
88+
project = skore.Project("hub://<tenant>/<name>")
89+
90+
assert isinstance(project._Project__project, skore_hub_project.Project)
91+
assert project.mode == "hub"
92+
assert project.name == "<name>"
93+
assert project._Project__project.tenant == "<tenant>"
94+
assert project._Project__project.name == "<name>"
95+
96+
for xp in (regression, classification, gridsearch):
97+
item = skore_hub_project.item.object_to_item(skore.EstimatorReport(**xp))
98+
99+
assert item.__metadata__
100+
assert item.__representation__
101+
assert item.__parameters__
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from sphinx_gallery.scrapers import matplotlib_scraper
22

33

4-
class matplotlib_skore_scraper: # defining matplotlib scraper as a class not a function
4+
class matplotlib_skore_scraper: # defining matplotlib scraper as a class not a function
55
def __call__(self, *args, **kwargs):
66
kwargs.setdefault("bbox_inches", "tight")
77
return matplotlib_scraper(*args, **kwargs)

0 commit comments

Comments
 (0)