Skip to content

Commit b22a0ab

Browse files
authored
Nox and uv integration upgrades (#1)
* WIP: nox with uv pipelines * WIP: Enforce uv setup venv * WIP: Force uv to use sessions interpreter * Drop python-version for coverage to conform with current requirements
1 parent 1c6bfa8 commit b22a0ab

File tree

2 files changed

+163
-64
lines changed

2 files changed

+163
-64
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ jobs:
107107
- name: Install the pinned version of uv
108108
uses: astral-sh/setup-uv@v5
109109
with:
110-
python-version: 3.13
110+
python-version: 3.12
111111
pyproject-file: "${{ github.workspace }}/pyproject.toml"
112112

113113
- name: Install Nox

noxfile.py

Lines changed: 162 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"""Nox sessions."""
22

33
import os
4+
import shlex
45
import shutil
56
import sys
67
from pathlib import Path
78
from tempfile import NamedTemporaryFile
9+
from textwrap import dedent
810

911
import nox
1012
from nox import Session
@@ -25,50 +27,147 @@
2527
)
2628

2729

30+
def session_install_uv(
31+
session: Session,
32+
install_project: bool = True,
33+
install_dev: bool = False,
34+
install_docs: bool = False,
35+
) -> None:
36+
"""Install root project into the session's virtual environment using uv."""
37+
env = os.environ.copy()
38+
env.update(
39+
{
40+
"UV_PROJECT_ENVIRONMENT": session.virtualenv.location,
41+
"UV_PYTHON": sys.executable, # Force uv to use the session's interpreter
42+
}
43+
)
44+
45+
args = ["uv", "sync", "--frozen"]
46+
if not install_project:
47+
args.append("--no-install-project")
48+
if not install_dev:
49+
args.append("--no-dev")
50+
if install_docs:
51+
args.extend(["--group", "docs"])
52+
53+
session.run_install(*args, silent=True, env=env)
54+
55+
56+
def session_install_uv_package(session: Session, packages: list[str]) -> None:
57+
"""Install packages into the session's virtual environment using uv lockfile."""
58+
env = os.environ.copy()
59+
env.update(
60+
{
61+
"UV_PROJECT_ENVIRONMENT": session.virtualenv.location,
62+
"UV_PYTHON": sys.executable,
63+
}
64+
)
65+
66+
# Export requirements.txt to session temp dir using uv with locked dependencies
67+
requirements_tmp = str(Path(session.create_tmp()) / "requirements.txt")
68+
export_args = ["uv", "export", "--only-dev", "--no-hashes", "-o", requirements_tmp]
69+
session.run_install(*export_args, silent=True, env=env)
70+
71+
# Install requested packages with requirements.txt constraints
72+
session.install(*packages, "--constraint", requirements_tmp)
73+
74+
75+
def activate_virtualenv_in_precommit_hooks(session: Session) -> None:
76+
"""Activate virtualenv in hooks installed by pre-commit.
77+
78+
This function patches git hooks installed by pre-commit to activate the
79+
session's virtual environment. This allows pre-commit to locate hooks in
80+
that environment when invoked from git.
81+
82+
Args:
83+
session: The Session object.
84+
"""
85+
assert session.bin is not None # noqa: S101
86+
87+
# Only patch hooks containing a reference to this session's bindir. Support
88+
# quoting rules for Python and bash, but strip the outermost quotes so we
89+
# can detect paths within the bindir, like <bindir>/python.
90+
bindirs = [
91+
bindir[1:-1] if bindir[0] in "'\"" else bindir
92+
for bindir in (repr(session.bin), shlex.quote(session.bin))
93+
]
94+
95+
virtualenv = session.env.get("VIRTUAL_ENV")
96+
if virtualenv is None:
97+
return
98+
99+
headers = {
100+
# pre-commit < 2.16.0
101+
"python": f"""\
102+
import os
103+
os.environ["VIRTUAL_ENV"] = {virtualenv!r}
104+
os.environ["PATH"] = os.pathsep.join((
105+
{session.bin!r},
106+
os.environ.get("PATH", ""),
107+
))
108+
""",
109+
# pre-commit >= 2.16.0
110+
"bash": f"""\
111+
VIRTUAL_ENV={shlex.quote(virtualenv)}
112+
PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH"
113+
""",
114+
# pre-commit >= 2.17.0 on Windows forces sh shebang
115+
"/bin/sh": f"""\
116+
VIRTUAL_ENV={shlex.quote(virtualenv)}
117+
PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH"
118+
""",
119+
}
120+
121+
hookdir = Path(".git") / "hooks"
122+
if not hookdir.is_dir():
123+
return
124+
125+
for hook in hookdir.iterdir():
126+
if hook.name.endswith(".sample") or not hook.is_file():
127+
continue
128+
129+
if not hook.read_bytes().startswith(b"#!"):
130+
continue
131+
132+
text = hook.read_text()
133+
134+
if not any(
135+
Path("A") == Path("a") and bindir.lower() in text.lower() or bindir in text
136+
for bindir in bindirs
137+
):
138+
continue
139+
140+
lines = text.splitlines()
141+
142+
for executable, header in headers.items():
143+
if executable in lines[0].lower():
144+
lines.insert(1, dedent(header))
145+
hook.write_text("\n".join(lines))
146+
break
147+
148+
28149
@session(name="pre-commit", python=python_versions[0])
29150
def precommit(session: Session) -> None:
30151
"""Lint using pre-commit."""
152+
session_install_uv(session, install_dev=True)
31153
args = session.posargs or [
32154
"run",
33155
"--all-files",
34156
"--hook-stage=manual",
35157
"--show-diff-on-failure",
36158
]
37-
session.run(
38-
"uv",
39-
"pip",
40-
"install",
41-
"black",
42-
"darglint",
43-
"flake8",
44-
"flake8-bandit",
45-
"flake8-bugbear",
46-
"flake8-docstrings",
47-
"flake8-rst-docstrings",
48-
"isort",
49-
"pep8-naming",
50-
"pre-commit",
51-
"pre-commit-hooks",
52-
"pyupgrade",
53-
external=True,
54-
)
159+
session_install_uv_package(session, ["pre-commit"])
55160
session.run("pre-commit", *args)
161+
if args and args[0] == "install":
162+
activate_virtualenv_in_precommit_hooks(session)
56163

57164

58165
@session(python=python_versions[0])
59166
def safety(session: Session) -> None:
60167
"""Scan dependencies for insecure packages."""
61168
with NamedTemporaryFile(delete=False) as requirements:
62-
session.run(
63-
"uv",
64-
"pip",
65-
"compile",
66-
"pyproject.toml",
67-
"--output-file",
68-
requirements.name,
69-
external=True,
70-
)
71-
session.run("uv", "pip", "install", "safety", external=True)
169+
session_install_uv(session, install_dev=True)
170+
session_install_uv_package(session, ["safety"])
72171
# TODO(Altay): Remove the CVE ignore once its resolved.
73172
# It's not critical, so ignoring now.
74173
ignore = ["70612"]
@@ -88,8 +187,8 @@ def safety(session: Session) -> None:
88187
def mypy(session: Session) -> None:
89188
"""Type-check using mypy."""
90189
args = session.posargs or ["src", "tests", "docs/conf.py"]
91-
session.run_always("uv", "pip", "install", "-e", ".", external=True)
92-
session.run("uv", "pip", "install", "mypy", "pytest", external=True)
190+
session_install_uv(session, install_dev=True)
191+
session_install_uv_package(session, ["mypy", "pytest"])
93192
session.run("mypy", *args)
94193
if not session.posargs:
95194
session.run("mypy", f"--python-executable={sys.executable}", "noxfile.py")
@@ -98,7 +197,10 @@ def mypy(session: Session) -> None:
98197
@session(python=python_versions)
99198
def tests(session: Session) -> None:
100199
"""Run the test suite."""
101-
session.run_always("uv", "pip", "install", "-e", ".[cloud]", external=True)
200+
session_install_uv(session, install_project=True, install_dev=True)
201+
session_install_uv_package(
202+
session, ["coverage", "pytest", "pygments", "pytest-dependency", "s3fs"]
203+
)
102204
session.run(
103205
"uv",
104206
"pip",
@@ -122,7 +224,8 @@ def coverage(session: Session) -> None:
122224
"""Produce the coverage report."""
123225
args = session.posargs or ["report"]
124226

125-
session.run("uv", "pip", "install", "coverage[toml]", external=True)
227+
session_install_uv(session, install_project=True, install_dev=True)
228+
session_install_uv_package(session, ["coverage"])
126229

127230
if not session.posargs and any(Path().glob(".coverage.*")):
128231
session.run("coverage", "combine", external=True)
@@ -133,10 +236,8 @@ def coverage(session: Session) -> None:
133236
@session(python=python_versions[0])
134237
def typeguard(session: Session) -> None:
135238
"""Runtime type checking using Typeguard."""
136-
session.run_always("uv", "pip", "install", "-e", ".", external=True)
137-
session.run(
138-
"uv", "pip", "install", "pytest", "typeguard", "pygments", external=True
139-
)
239+
session_install_uv(session, install_project=True, install_dev=True)
240+
session_install_uv_package(session, ["pytest", "typeguard", "pygments"])
140241
session.run("pytest", f"--typeguard-packages={package}", *session.posargs)
141242

142243

@@ -150,8 +251,8 @@ def xdoctest(session: Session) -> None:
150251
if "FORCE_COLOR" in os.environ:
151252
args.append("--colored=1")
152253

153-
session.run_always("uv", "pip", "install", "-e", ".", external=True)
154-
session.run("uv", "pip", "install", "xdoctest[colors]", external=True)
254+
session_install_uv(session, install_project=True, install_dev=True)
255+
session_install_uv_package(session, ["xdoctest"])
155256
session.run("python", "-m", "xdoctest", *args)
156257

157258

@@ -162,18 +263,17 @@ def docs_build(session: Session) -> None:
162263
if not session.posargs and "FORCE_COLOR" in os.environ:
163264
args.insert(0, "--color")
164265

165-
session.run_always("uv", "pip", "install", "-e", ".", external=True)
166-
session.run(
167-
"uv",
168-
"pip",
169-
"install",
170-
"sphinx",
171-
"sphinx-click",
172-
"sphinx-copybutton",
173-
"furo",
174-
"myst-nb",
175-
"linkify-it-py",
176-
external=True,
266+
session_install_uv(session, install_project=True, install_dev=True)
267+
session_install_uv_package(
268+
session,
269+
[
270+
"sphinx",
271+
"sphinx-click",
272+
"sphinx-copybutton",
273+
"furo",
274+
"myst-nb",
275+
"linkify-it-py",
276+
],
177277
)
178278

179279
build_dir = Path("docs", "_build")
@@ -187,19 +287,18 @@ def docs_build(session: Session) -> None:
187287
def docs(session: Session) -> None:
188288
"""Build and serve the documentation with live reloading on file changes."""
189289
args = session.posargs or ["--open-browser", "docs", "docs/_build"]
190-
session.run_always("uv", "pip", "install", "-e", ".", external=True)
191-
session.run(
192-
"uv",
193-
"pip",
194-
"install",
195-
"sphinx",
196-
"sphinx-autobuild",
197-
"sphinx-click",
198-
"sphinx-copybutton",
199-
"furo",
200-
"myst-nb",
201-
"linkify-it-py",
202-
external=True,
290+
session_install_uv(session, install_project=True, install_dev=True)
291+
session_install_uv_package(
292+
session,
293+
[
294+
"sphinx",
295+
"sphinx-autobuild",
296+
"sphinx-click",
297+
"sphinx-copybutton",
298+
"furo",
299+
"myst-nb",
300+
"linkify-it-py",
301+
],
203302
)
204303

205304
build_dir = Path("docs", "_build")

0 commit comments

Comments
 (0)