Skip to content

Commit 5c52b87

Browse files
committed
Create noxfile.py
1 parent a1e0be0 commit 5c52b87

File tree

1 file changed

+291
-0
lines changed

1 file changed

+291
-0
lines changed

noxfile.py

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
"""Perform test automation with nox.
2+
3+
For further details, see https://nox.thea.codes/en/stable/#
4+
5+
"""
6+
7+
import hashlib
8+
import os
9+
from pathlib import Path
10+
11+
import nox
12+
from nox.logger import logger
13+
14+
#: Default to reusing any pre-existing nox environments.
15+
nox.options.reuse_existing_virtualenvs = True
16+
17+
#: Python versions we can run sessions under
18+
_PY_VERSIONS_ALL = ["3.11", "3.12", "3.13"]
19+
_PY_VERSION_LATEST = _PY_VERSIONS_ALL[-1]
20+
21+
#: One specific python version for docs builds
22+
_PY_VERSION_DOCSBUILD = _PY_VERSION_LATEST
23+
24+
#: Cirrus-CI environment variable hook.
25+
PY_VER = os.environ.get("PY_VER", _PY_VERSIONS_ALL)
26+
27+
#: Default cartopy cache directory.
28+
CARTOPY_CACHE_DIR = os.environ.get("HOME") / Path(".local/share/cartopy")
29+
30+
# https://github.com/numpy/numpy/pull/19478
31+
# https://github.com/matplotlib/matplotlib/pull/22099
32+
#: Common session environment variables.
33+
ENV = dict(NPY_DISABLE_CPU_FEATURES="AVX512F,AVX512CD,AVX512_SKX")
34+
35+
36+
def session_lockfile(session: nox.sessions.Session) -> Path:
37+
"""Return the path of the session lockfile."""
38+
return Path(f"requirements/locks/py{session.python.replace('.', '')}-linux-64.lock")
39+
40+
41+
def session_cachefile(session: nox.sessions.Session) -> Path:
42+
"""Return the path of the session lockfile cache."""
43+
lockfile = session_lockfile(session)
44+
tmp_dir = Path(session.create_tmp())
45+
cache = tmp_dir / lockfile.name
46+
return cache
47+
48+
49+
def venv_populated(session: nox.sessions.Session) -> bool:
50+
"""List of packages in the lockfile installed.
51+
52+
Returns True if the conda venv has been created.
53+
"""
54+
return session_cachefile(session).is_file()
55+
56+
57+
def venv_changed(session: nox.sessions.Session) -> bool:
58+
"""Return True if the installed session is different.
59+
60+
Compares to that specified in the lockfile.
61+
"""
62+
changed = False
63+
cache = session_cachefile(session)
64+
lockfile = session_lockfile(session)
65+
if cache.is_file():
66+
with open(lockfile, "rb") as fi:
67+
expected = hashlib.sha256(fi.read()).hexdigest()
68+
with open(cache, "r") as fi:
69+
actual = fi.read()
70+
changed = actual != expected
71+
return changed
72+
73+
74+
def cache_venv(session: nox.sessions.Session) -> None:
75+
"""Cache the nox session environment.
76+
77+
This consists of saving a hexdigest (sha256) of the associated
78+
conda lock file.
79+
80+
Parameters
81+
----------
82+
session : object
83+
A `nox.sessions.Session` object.
84+
85+
"""
86+
lockfile = session_lockfile(session)
87+
cache = session_cachefile(session)
88+
with open(lockfile, "rb") as fi:
89+
hexdigest = hashlib.sha256(fi.read()).hexdigest()
90+
with open(cache, "w") as fout:
91+
fout.write(hexdigest)
92+
93+
94+
def cache_cartopy(session: nox.sessions.Session) -> None:
95+
"""Determine whether to cache the cartopy natural earth shapefiles.
96+
97+
Parameters
98+
----------
99+
session : object
100+
A `nox.sessions.Session` object.
101+
102+
"""
103+
if not CARTOPY_CACHE_DIR.is_dir():
104+
session.run_always(
105+
"python",
106+
"-c",
107+
"import cartopy; cartopy.io.shapereader.natural_earth()",
108+
)
109+
110+
111+
def prepare_venv(session: nox.sessions.Session) -> None:
112+
"""Create and cache the nox session conda environment.
113+
114+
Additionally provide conda environment package details and info.
115+
116+
Note that, iris is installed into the environment using pip.
117+
118+
Parameters
119+
----------
120+
session : object
121+
A `nox.sessions.Session` object.
122+
123+
Notes
124+
-----
125+
See
126+
- https://github.com/theacodes/nox/issues/346
127+
- https://github.com/theacodes/nox/issues/260
128+
129+
"""
130+
lockfile = session_lockfile(session)
131+
venv_dir = session.virtualenv.location_name
132+
133+
if not venv_populated(session):
134+
# environment has been created but packages not yet installed
135+
# populate the environment from the lockfile
136+
logger.debug(f"Populating conda env at {venv_dir}")
137+
session.conda_install("--file", str(lockfile))
138+
cache_venv(session)
139+
140+
elif venv_changed(session):
141+
# destroy the environment and rebuild it
142+
logger.debug(f"Lockfile changed. Re-creating conda env at {venv_dir}")
143+
_re_orig = session.virtualenv.reuse_existing
144+
session.virtualenv.reuse_existing = False
145+
session.virtualenv.create()
146+
session.conda_install("--file", str(lockfile))
147+
session.virtualenv.reuse_existing = _re_orig
148+
cache_venv(session)
149+
150+
logger.debug(f"Environment {venv_dir} is up to date")
151+
152+
cache_cartopy(session)
153+
154+
# Determine whether verbose diagnostics have been requested
155+
# from the command line.
156+
verbose = "-v" in session.posargs or "--verbose" in session.posargs
157+
158+
if verbose:
159+
session.run_always("conda", "info")
160+
session.run_always("conda", "list", f"--prefix={venv_dir}")
161+
session.run_always(
162+
"conda",
163+
"list",
164+
f"--prefix={venv_dir}",
165+
"--explicit",
166+
)
167+
168+
169+
@nox.session(python=PY_VER, venv_backend="conda")
170+
def tests(session: nox.sessions.Session):
171+
"""Perform iris system, integration and unit tests.
172+
173+
Coverage testing is enabled if the "--coverage" or "-c" flag is used.
174+
175+
Parameters
176+
----------
177+
session : object
178+
A `nox.sessions.Session` object.
179+
180+
"""
181+
prepare_venv(session)
182+
session.install("--no-deps", "--editable", ".")
183+
session.env.update(ENV)
184+
run_args = [
185+
"pytest",
186+
"-n",
187+
"auto",
188+
"lib/iris/tests",
189+
]
190+
if "-c" in session.posargs or "--coverage" in session.posargs:
191+
run_args[-1:-1] = ["--cov=lib/iris", "--cov-report=xml"]
192+
session.run(*run_args)
193+
194+
195+
@nox.session(python=_PY_VERSION_DOCSBUILD, venv_backend="conda")
196+
def doctest(session: nox.sessions.Session):
197+
"""Perform iris doctests and gallery.
198+
199+
Parameters
200+
----------
201+
session : object
202+
A `nox.sessions.Session` object.
203+
204+
"""
205+
prepare_venv(session)
206+
session.install("--no-deps", "--editable", ".")
207+
session.env.update(ENV)
208+
session.cd("docs")
209+
session.run(
210+
"make",
211+
"clean",
212+
"html",
213+
external=True,
214+
)
215+
session.run(
216+
"make",
217+
"doctest",
218+
external=True,
219+
)
220+
221+
222+
@nox.session(python=_PY_VERSION_DOCSBUILD, venv_backend="conda")
223+
def gallery(session: nox.sessions.Session):
224+
"""Perform iris gallery doc-tests.
225+
226+
Parameters
227+
----------
228+
session : object
229+
A `nox.sessions.Session` object.
230+
231+
"""
232+
prepare_venv(session)
233+
session.install("--no-deps", "--editable", ".")
234+
session.env.update(ENV)
235+
session.run(
236+
"pytest",
237+
"-n",
238+
"auto",
239+
"docs/gallery_tests",
240+
)
241+
242+
243+
@nox.session(python=PY_VER, venv_backend="conda")
244+
def wheel(session: nox.sessions.Session):
245+
"""Perform iris local wheel install and import test.
246+
247+
Parameters
248+
----------
249+
session : object
250+
A `nox.sessions.Session` object.
251+
252+
"""
253+
prepare_venv(session)
254+
session.cd("dist")
255+
fname = list(Path(".").glob("scitools_iris-*.whl"))
256+
if len(fname) == 0:
257+
raise ValueError("Cannot find wheel to install.")
258+
if len(fname) > 1:
259+
emsg = f"Expected to find 1 wheel to install, found {len(fname)} instead."
260+
raise ValueError(emsg)
261+
session.install(fname[0].name)
262+
session.run(
263+
"python",
264+
"-c",
265+
"import iris; print(f'{iris.__version__=}')",
266+
external=True,
267+
)
268+
269+
270+
@nox.session
271+
def benchmarks(session: nox.sessions.Session):
272+
"""Run the Iris benchmark runner. Run session with `-- --help` for help.
273+
274+
Parameters
275+
----------
276+
session : object
277+
A `nox.sessions.Session` object.
278+
279+
"""
280+
if len(session.posargs) == 0:
281+
message = (
282+
"This session MUST be run with at least one argument. The "
283+
"arguments are passed down to the benchmark runner script. E.g:\n"
284+
"nox -s benchmarks -- --help\n"
285+
"nox -s benchmarks -- something --help\n"
286+
"nox -s benchmarks -- something\n"
287+
)
288+
session.error(message)
289+
session.install("asv", "nox")
290+
bm_runner_path = Path(__file__).parent / "benchmarks" / "bm_runner.py"
291+
session.run("python", bm_runner_path, *session.posargs)

0 commit comments

Comments
 (0)