Skip to content

Commit 11478ba

Browse files
committed
Add nox setup
1 parent be34fec commit 11478ba

1 file changed

Lines changed: 374 additions & 0 deletions

File tree

‎noxfile.py‎

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import os
5+
import re
6+
import shlex
7+
import shutil
8+
import tempfile
9+
from pathlib import Path
10+
11+
import nox
12+
13+
PROJECT_ROOT = Path(__file__).parent
14+
PYPROJECT_PATH = PROJECT_ROOT / "pyproject.toml"
15+
16+
nox.options.reuse_existing_virtualenvs = True
17+
nox.options.sessions = ["tests"]
18+
19+
20+
def _load_pyproject() -> dict:
21+
try:
22+
import tomllib
23+
except ImportError: # pragma: no cover - py<3.11
24+
import tomli as tomllib
25+
with PYPROJECT_PATH.open("rb") as f:
26+
return tomllib.load(f)
27+
28+
29+
def _version_range(requirement: str) -> list[str]:
30+
min_match = re.search(r">=(\d+\.\d+)", requirement)
31+
max_match = re.search(r"<(\d+\.\d+)", requirement)
32+
if not (min_match and max_match):
33+
return []
34+
min_v = tuple(map(int, min_match.group(1).split(".")))
35+
max_v = tuple(map(int, max_match.group(1).split(".")))
36+
versions = []
37+
current = min_v
38+
while current < max_v:
39+
versions.append(".".join(map(str, current)))
40+
current = (current[0], current[1] + 1)
41+
return versions
42+
43+
44+
def _matrix_versions() -> tuple[list[str], list[str]]:
45+
data = _load_pyproject()
46+
python_req = data["project"]["requires-python"]
47+
py_versions = _version_range(python_req)
48+
mpl_req = next(
49+
dep for dep in data["project"]["dependencies"] if dep.startswith("matplotlib")
50+
)
51+
mpl_versions = _version_range(mpl_req) or ["3.9"]
52+
return py_versions, mpl_versions
53+
54+
55+
PYTHON_VERSIONS, MPL_VERSIONS = _matrix_versions()
56+
57+
58+
def _mamba_root() -> Path:
59+
return PROJECT_ROOT / ".nox" / "micromamba"
60+
61+
62+
def _mamba_exe(session: nox.Session) -> str:
63+
exe = os.environ.get("MAMBA_EXE", "micromamba")
64+
if shutil.which(exe):
65+
return exe
66+
session.error(
67+
"micromamba not found; install it or set MAMBA_EXE to the micromamba path."
68+
)
69+
return exe
70+
71+
72+
def _mamba_env_name(python_version: str, matplotlib_version: str) -> str:
73+
return f"ultraplot-py{python_version}-mpl{matplotlib_version}"
74+
75+
76+
def _ensure_mamba_env(
77+
session: nox.Session, python_version: str, matplotlib_version: str
78+
) -> str:
79+
root = _mamba_root()
80+
env_name = _mamba_env_name(python_version, matplotlib_version)
81+
env_path = root / "envs" / env_name
82+
if env_path.exists():
83+
return env_name
84+
exe = _mamba_exe(session)
85+
env = os.environ.copy()
86+
env["MAMBA_ROOT_PREFIX"] = str(root)
87+
session.run(
88+
exe,
89+
"create",
90+
"-y",
91+
"-n",
92+
env_name,
93+
"-f",
94+
str(PROJECT_ROOT / "environment.yml"),
95+
f"python={python_version}",
96+
f"matplotlib={matplotlib_version}",
97+
external=True,
98+
env=env,
99+
)
100+
return env_name
101+
102+
103+
def _mamba_run(session: nox.Session, env_name: str, *args: str) -> None:
104+
exe = _mamba_exe(session)
105+
env = os.environ.copy()
106+
env["MAMBA_ROOT_PREFIX"] = str(_mamba_root())
107+
quoted = " ".join(shlex.quote(arg) for arg in args)
108+
session.run(
109+
"bash",
110+
"-lc",
111+
f'eval "$({shlex.quote(exe)} shell hook -s bash)"; '
112+
f"micromamba activate {shlex.quote(env_name)}; {quoted}",
113+
external=True,
114+
env=env,
115+
)
116+
117+
118+
def _install_ultraplot(session: nox.Session, env_name: str, path: str) -> None:
119+
_mamba_run(
120+
session,
121+
env_name,
122+
"python",
123+
"-m",
124+
"pip",
125+
"install",
126+
"--no-build-isolation",
127+
"--no-deps",
128+
path,
129+
)
130+
131+
132+
def _selected_nodeids(env: dict[str, str]) -> list[str] | None:
133+
if env.get("TEST_MODE", "full") != "selected":
134+
return None
135+
tokens = env.get("TEST_NODEIDS", "").split()
136+
nodeids = [t for t in tokens if "::" in t or t.endswith(".py")]
137+
return nodeids or None
138+
139+
140+
@nox.session(venv_backend="none")
141+
@nox.parametrize("python_version", PYTHON_VERSIONS)
142+
@nox.parametrize("matplotlib_version", MPL_VERSIONS)
143+
def tests(session: nox.Session, python_version: str, matplotlib_version: str) -> None:
144+
env_name = _ensure_mamba_env(session, python_version, matplotlib_version)
145+
_install_ultraplot(session, env_name, ".")
146+
nodeids = _selected_nodeids(session.env)
147+
if nodeids:
148+
_mamba_run(
149+
session,
150+
env_name,
151+
"pytest",
152+
"--cov=ultraplot",
153+
"--cov-branch",
154+
"--cov-report",
155+
"term-missing",
156+
"--cov-report=xml",
157+
*nodeids,
158+
)
159+
else:
160+
_mamba_run(
161+
session,
162+
env_name,
163+
"pytest",
164+
"--cov=ultraplot",
165+
"--cov-branch",
166+
"--cov-report",
167+
"term-missing",
168+
"--cov-report=xml",
169+
"ultraplot",
170+
)
171+
172+
173+
@nox.session
174+
def select_tests(session: nox.Session) -> None:
175+
if len(session.posargs) >= 2:
176+
base, head = session.posargs[:2]
177+
else:
178+
base, head = "origin/main", "HEAD"
179+
ci_dir = PROJECT_ROOT / ".ci"
180+
ci_dir.mkdir(parents=True, exist_ok=True)
181+
changed = ci_dir / "changed.txt"
182+
with changed.open("w", encoding="utf-8") as changed_file:
183+
session.run(
184+
"git",
185+
"diff",
186+
"--name-only",
187+
base,
188+
head,
189+
external=True,
190+
stdout=changed_file,
191+
)
192+
selection = ci_dir / "selection.json"
193+
session.run(
194+
"python",
195+
"tools/ci/select_tests.py",
196+
"--map",
197+
str(ci_dir / "test-map.json"),
198+
"--changed-files",
199+
str(changed),
200+
"--output",
201+
str(selection),
202+
"--always-full",
203+
"pyproject.toml",
204+
"--always-full",
205+
"environment.yml",
206+
"--always-full",
207+
"ultraplot/__init__.py",
208+
"--ignore",
209+
"docs/**",
210+
"--ignore",
211+
"README.rst",
212+
)
213+
data = json.loads(selection.read_text(encoding="utf-8"))
214+
session.log("mode=%s", data.get("mode"))
215+
session.log("tests=%s", " ".join(data.get("tests", [])))
216+
217+
218+
@nox.session
219+
def build_test_map(session: nox.Session) -> None:
220+
env_name = _ensure_mamba_env(session, "3.11", "3.9")
221+
_install_ultraplot(session, env_name, ".")
222+
ci_dir = PROJECT_ROOT / ".ci"
223+
ci_dir.mkdir(parents=True, exist_ok=True)
224+
_mamba_run(
225+
session,
226+
env_name,
227+
"pytest",
228+
"-n",
229+
"auto",
230+
"--cov=ultraplot",
231+
"--cov-branch",
232+
"--cov-context=test",
233+
"--cov-report=",
234+
"ultraplot",
235+
)
236+
_mamba_run(
237+
session,
238+
env_name,
239+
"python",
240+
"tools/ci/build_test_map.py",
241+
"--coverage-file",
242+
".coverage",
243+
"--output",
244+
str(ci_dir / "test-map.json"),
245+
"--root",
246+
".",
247+
)
248+
249+
250+
@nox.session(venv_backend="none")
251+
@nox.parametrize("python_version", PYTHON_VERSIONS)
252+
@nox.parametrize("matplotlib_version", MPL_VERSIONS)
253+
def compare_baseline(
254+
session: nox.Session, python_version: str, matplotlib_version: str
255+
) -> None:
256+
base_ref = session.env.get("BASE_REF", "origin/main")
257+
baseline_dir = Path(session.env.get("BASELINE_DIR", "ultraplot/tests/baseline"))
258+
results_dir = Path(session.env.get("RESULTS_DIR", "results"))
259+
baseline_dir.mkdir(parents=True, exist_ok=True)
260+
results_dir.mkdir(parents=True, exist_ok=True)
261+
262+
env_name = _ensure_mamba_env(session, python_version, matplotlib_version)
263+
_install_ultraplot(session, env_name, ".")
264+
nodeids = _selected_nodeids(session.env)
265+
266+
with tempfile.TemporaryDirectory() as tmpdir:
267+
session.run(
268+
"git",
269+
"worktree",
270+
"add",
271+
"--detach",
272+
tmpdir,
273+
base_ref,
274+
external=True,
275+
)
276+
try:
277+
_install_ultraplot(session, env_name, tmpdir)
278+
_mamba_run(
279+
session,
280+
env_name,
281+
"python",
282+
"-c",
283+
"import ultraplot as plt; plt.config.Configurator()._save_yaml('ultraplot.yml')",
284+
)
285+
if nodeids:
286+
_mamba_run(
287+
session,
288+
env_name,
289+
"pytest",
290+
"-W",
291+
"ignore",
292+
"--mpl-generate-path",
293+
str(baseline_dir),
294+
"--mpl-default-style=./ultraplot.yml",
295+
*nodeids,
296+
)
297+
else:
298+
_mamba_run(
299+
session,
300+
env_name,
301+
"pytest",
302+
"-W",
303+
"ignore",
304+
"--mpl-generate-path",
305+
str(baseline_dir),
306+
"--mpl-default-style=./ultraplot.yml",
307+
"ultraplot/tests",
308+
)
309+
finally:
310+
session.run(
311+
"git",
312+
"worktree",
313+
"remove",
314+
"--force",
315+
tmpdir,
316+
external=True,
317+
)
318+
session.run("git", "worktree", "prune", external=True)
319+
320+
_install_ultraplot(session, env_name, ".")
321+
_mamba_run(
322+
session,
323+
env_name,
324+
"python",
325+
"-c",
326+
"import ultraplot as plt; plt.config.Configurator()._save_yaml('ultraplot.yml')",
327+
)
328+
if nodeids:
329+
_mamba_run(
330+
session,
331+
env_name,
332+
"pytest",
333+
"-W",
334+
"ignore",
335+
"--mpl",
336+
"--mpl-baseline-path",
337+
str(baseline_dir),
338+
"--mpl-results-path",
339+
str(results_dir),
340+
"--mpl-generate-summary=html",
341+
"--mpl-default-style=./ultraplot.yml",
342+
*nodeids,
343+
)
344+
else:
345+
_mamba_run(
346+
session,
347+
env_name,
348+
"pytest",
349+
"-W",
350+
"ignore",
351+
"--mpl",
352+
"--mpl-baseline-path",
353+
str(baseline_dir),
354+
"--mpl-results-path",
355+
str(results_dir),
356+
"--mpl-generate-summary=html",
357+
"--mpl-default-style=./ultraplot.yml",
358+
"ultraplot/tests",
359+
)
360+
361+
362+
@nox.session
363+
def build_dist(session: nox.Session) -> None:
364+
session.install(
365+
"--upgrade", "pip", "wheel", "setuptools", "setuptools_scm", "build", "twine"
366+
)
367+
session.run("python", "-m", "build", "--sdist", "--wheel", ".", "--outdir", "dist")
368+
session.run("python", "-m", "pip", "install", "dist/ultraplot*.whl")
369+
session.run(
370+
"python",
371+
"-c",
372+
"import ultraplot as u; assert not u.__version__.startswith('0.'), u.__version__",
373+
)
374+
session.run("python", "-m", "twine", "check", "dist/*")

0 commit comments

Comments
 (0)