Skip to content

Commit b7fd291

Browse files
committed
WIP: nox with uv pipelines
1 parent ad725be commit b7fd291

File tree

1 file changed

+150
-63
lines changed

1 file changed

+150
-63
lines changed

noxfile.py

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

57152

58153
@session(python=python_versions[0])
59154
def safety(session: Session) -> None:
60155
"""Scan dependencies for insecure packages."""
61156
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)
157+
session_install_uv(session, install_dev=True)
158+
session_install_uv_package(session, ["safety"])
72159
# TODO(Altay): Remove the CVE ignore once its resolved.
73160
# It's not critical, so ignoring now.
74161
ignore = ["70612"]
@@ -88,8 +175,8 @@ def safety(session: Session) -> None:
88175
def mypy(session: Session) -> None:
89176
"""Type-check using mypy."""
90177
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)
178+
session_install_uv(session, install_dev=True)
179+
session_install_uv_package(session, ["mypy", "pytest"])
93180
session.run("mypy", *args)
94181
if not session.posargs:
95182
session.run("mypy", f"--python-executable={sys.executable}", "noxfile.py")
@@ -98,7 +185,10 @@ def mypy(session: Session) -> None:
98185
@session(python=python_versions)
99186
def tests(session: Session) -> None:
100187
"""Run the test suite."""
101-
session.run_always("uv", "pip", "install", "-e", ".[cloud]", external=True)
188+
session_install_uv(session, install_project=True, install_dev=True)
189+
session_install_uv_package(
190+
session, ["coverage", "pytest", "pygments", "pytest-dependency", "s3fs"]
191+
)
102192
session.run(
103193
"uv",
104194
"pip",
@@ -122,7 +212,8 @@ def coverage(session: Session) -> None:
122212
"""Produce the coverage report."""
123213
args = session.posargs or ["report"]
124214

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

127218
if not session.posargs and any(Path().glob(".coverage.*")):
128219
session.run("coverage", "combine", external=True)
@@ -133,10 +224,8 @@ def coverage(session: Session) -> None:
133224
@session(python=python_versions[0])
134225
def typeguard(session: Session) -> None:
135226
"""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-
)
227+
session_install_uv(session, install_project=True, install_dev=True)
228+
session_install_uv_package(session, ["pytest", "typeguard", "pygments"])
140229
session.run("pytest", f"--typeguard-packages={package}", *session.posargs)
141230

142231

@@ -150,8 +239,8 @@ def xdoctest(session: Session) -> None:
150239
if "FORCE_COLOR" in os.environ:
151240
args.append("--colored=1")
152241

153-
session.run_always("uv", "pip", "install", "-e", ".", external=True)
154-
session.run("uv", "pip", "install", "xdoctest[colors]", external=True)
242+
session_install_uv(session, install_project=True, install_dev=True)
243+
session_install_uv_package(session, ["xdoctest"])
155244
session.run("python", "-m", "xdoctest", *args)
156245

157246

@@ -162,18 +251,17 @@ def docs_build(session: Session) -> None:
162251
if not session.posargs and "FORCE_COLOR" in os.environ:
163252
args.insert(0, "--color")
164253

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,
254+
session_install_uv(session, install_project=True, install_dev=True)
255+
session_install_uv_package(
256+
session,
257+
[
258+
"sphinx",
259+
"sphinx-click",
260+
"sphinx-copybutton",
261+
"furo",
262+
"myst-nb",
263+
"linkify-it-py",
264+
],
177265
)
178266

179267
build_dir = Path("docs", "_build")
@@ -187,19 +275,18 @@ def docs_build(session: Session) -> None:
187275
def docs(session: Session) -> None:
188276
"""Build and serve the documentation with live reloading on file changes."""
189277
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,
278+
session_install_uv(session, install_project=True, install_dev=True)
279+
session_install_uv_package(
280+
session,
281+
[
282+
"sphinx",
283+
"sphinx-autobuild",
284+
"sphinx-click",
285+
"sphinx-copybutton",
286+
"furo",
287+
"myst-nb",
288+
"linkify-it-py",
289+
],
203290
)
204291

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

0 commit comments

Comments
 (0)