Skip to content

Commit b760c10

Browse files
authored
Improve nox dependency installation (#591)
* Provide our own function for nox to install with uv * Remove constraints.txt to simplify workflow * Fix method mock * Use nox to generate test matrix * Fix session spec * Upgrade dependencies * Add nox argument --reuse-venv=yes * Add workflow permissions
1 parent 6729e29 commit b760c10

File tree

8 files changed

+894
-1129
lines changed

8 files changed

+894
-1129
lines changed

.github/dependabot.yml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,6 @@ updates:
1111
github-action-dependencies:
1212
patterns:
1313
- "*"
14-
- package-ecosystem: "pip"
15-
directory: "/.github/workflows"
16-
schedule:
17-
interval: "monthly"
18-
open-pull-requests-limit: 99
19-
groups:
20-
workflows-dependencies:
21-
patterns:
22-
- "*"
2314
- package-ecosystem: "uv"
2415
directory: "/"
2516
schedule:

.github/workflows/constraints.txt

Lines changed: 0 additions & 2 deletions
This file was deleted.

.github/workflows/tests.yml

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,28 @@ on:
1313
- ".pre-commit-config.yaml"
1414
- "noxfile.py"
1515
- ".github/workflows/tests.yml"
16+
permissions:
17+
contents: read
1618

1719
jobs:
20+
generate-jobs:
21+
runs-on: ubuntu-latest
22+
outputs:
23+
session: ${{ steps.set-matrix.outputs.session }}
24+
steps:
25+
- uses: actions/checkout@v6
26+
- name: Install uv
27+
uses: astral-sh/setup-uv@v7
28+
- id: set-matrix
29+
run: echo session=$(uvx nox@2025.11.12 --json -l | jq -c '[.[].session]') | tee --append $GITHUB_OUTPUT
1830
tests:
19-
name: ${{ matrix.session }} ${{ matrix.python }} / ${{ matrix.os }}
20-
runs-on: ${{ matrix.os }}
31+
name: ${{ matrix.session }}
32+
needs: [generate-jobs]
33+
runs-on: ubuntu-latest
2134
strategy:
2235
fail-fast: false
2336
matrix:
24-
include:
25-
- { python: "3.13", os: "ubuntu-latest", session: "pre-commit" }
26-
- { python: "3.12", os: "ubuntu-latest", session: "mypy" }
27-
- { python: "3.13", os: "ubuntu-latest", session: "mypy" }
28-
- { python: "3.12", os: "ubuntu-latest", session: "tests" }
29-
- { python: "3.13", os: "ubuntu-latest", session: "tests" }
30-
- { python: "3.13", os: "ubuntu-latest", session: "typeguard" }
31-
- { python: "3.13", os: "ubuntu-latest", session: "xdoctest" }
32-
- { python: "3.13", os: "ubuntu-latest", session: "docs-build" }
37+
session: ${{ fromJson(needs.generate-jobs.outputs.session) }}
3338

3439
env:
3540
NOXSESSION: ${{ matrix.session }}
@@ -39,12 +44,6 @@ jobs:
3944
steps:
4045
- name: Check out the repository
4146
uses: actions/checkout@v6
42-
43-
- name: Set up Python ${{ matrix.python }}
44-
uses: actions/setup-python@v6.1.0
45-
with:
46-
python-version: ${{ matrix.python }}
47-
4847
- name: Install uv
4948
uses: astral-sh/setup-uv@v7
5049
with:
@@ -77,13 +76,13 @@ jobs:
7776
7877
- name: Run Nox
7978
run: |
80-
uvx --constraints "${{ github.workspace }}/.github/workflows/constraints.txt" nox --python=${{ matrix.python }}
79+
uvx nox@2025.11.12 --session=${{ matrix.session }}
8180
8281
- name: Upload coverage data
83-
if: always() && matrix.session == 'tests'
82+
if: always() && contains(matrix.session, 'tests')
8483
uses: "actions/upload-artifact@v6"
8584
with:
86-
name: coverage-data-${{ matrix.os }}-${{ matrix.python }}
85+
name: coverage-data-${{ matrix.session }}
8786
path: ".coverage.*"
8887
include-hidden-files: true
8988

@@ -116,11 +115,11 @@ jobs:
116115

117116
- name: Combine coverage data and display human readable report
118117
run: |
119-
uvx --constraints "${{ github.workspace }}/.github/workflows/constraints.txt" nox --session=coverage
118+
uvx nox@2025.11.12 --reuse-venv=yes --session=coverage
120119
121120
- name: Create coverage report
122121
run: |
123-
uvx --constraints "${{ github.workspace }}/.github/workflows/constraints.txt" nox --session=coverage -- xml
122+
uvx nox@2025.11.12 --reuse-venv=yes --session=coverage -- xml
124123
125124
# Need to fix coverage source paths for SonarCloud scanning in GitHub actions.
126125
# Replace root path with /github/workspace (mounted in docker container).

noxfile.py

Lines changed: 36 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import nox
1111

1212
package = "datadoc_editor"
13-
python_versions = ["3.12", "3.13"]
13+
python_versions = ["3.12", "3.13", "3.14"]
1414
nox.needs_version = ">= 2021.6.6"
1515
nox.options.default_venv_backend = "uv"
1616
nox.options.sessions = (
@@ -23,136 +23,61 @@
2323
)
2424

2525

26-
def activate_virtualenv_in_precommit_hooks(session: nox.Session) -> None:
27-
"""Activate virtualenv in hooks installed by pre-commit.
28-
29-
This function patches git hooks installed by pre-commit to activate the
30-
session's virtual environment. This allows pre-commit to locate hooks in
31-
that environment when invoked from git.
32-
33-
Args:
34-
session: The Session object.
35-
"""
36-
assert session.bin is not None # nosec
37-
38-
# Only patch hooks containing a reference to this session's bindir. Support
39-
# quoting rules for Python and bash, but strip the outermost quotes so we
40-
# can detect paths within the bindir, like <bindir>/python.
41-
bindirs = [
42-
bindir[1:-1] if bindir[0] in "'\"" else bindir
43-
for bindir in (repr(session.bin), shlex.quote(session.bin))
44-
]
45-
46-
virtualenv = session.env.get("VIRTUAL_ENV")
47-
if virtualenv is None:
48-
return
49-
50-
headers = {
51-
# pre-commit < 2.16.0
52-
"python": f"""\
53-
import os
54-
os.environ["VIRTUAL_ENV"] = {virtualenv!r}
55-
os.environ["PATH"] = os.pathsep.join((
56-
{session.bin!r},
57-
os.environ.get("PATH", ""),
58-
))
59-
""",
60-
# pre-commit >= 2.16.0
61-
"bash": f"""\
62-
VIRTUAL_ENV={shlex.quote(virtualenv)}
63-
PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH"
64-
""",
65-
# pre-commit >= 2.17.0 on Windows forces sh shebang
66-
"/bin/sh": f"""\
67-
VIRTUAL_ENV={shlex.quote(virtualenv)}
68-
PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH"
69-
""",
70-
}
71-
72-
hookdir = Path(".git") / "hooks"
73-
if not hookdir.is_dir():
74-
return
75-
76-
for hook in hookdir.iterdir():
77-
if hook.name.endswith(".sample") or not hook.is_file():
78-
continue
79-
80-
if not hook.read_bytes().startswith(b"#!"):
81-
continue
82-
83-
text = hook.read_text()
84-
85-
if not is_bindir_in_text(bindirs, text):
86-
continue
87-
88-
lines = text.splitlines()
89-
hook.write_text(insert_header_in_hook(headers, lines))
90-
91-
92-
def is_bindir_in_text(bindirs: list[str], text: str) -> bool:
93-
"""Helper function to check if bindir is in text."""
94-
return any(
95-
Path("A") == Path("a") and bindir.lower() in text.lower() or bindir in text
96-
for bindir in bindirs
26+
def install_with_uv(
27+
session: nox.Session,
28+
*,
29+
groups: list[str] | None = None,
30+
only_groups: list[str] | None = None,
31+
all_extras: bool = False,
32+
locked: bool = True,
33+
) -> None:
34+
"""Install packages using uv, pinned to uv.lock."""
35+
cmd = ["uv", "sync", "--no-default-groups"]
36+
if locked:
37+
cmd.append("--locked")
38+
if groups:
39+
for group in groups:
40+
cmd.extend(["--group", group])
41+
if only_groups:
42+
for group in only_groups or []:
43+
cmd.extend(["--only-group", group])
44+
if all_extras:
45+
cmd.append("--all-extras")
46+
cmd.append(
47+
f"--python={session.virtualenv.location}"
48+
) # Target the nox venv's Python interpreter
49+
session.run_install(
50+
*cmd, env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}
9751
)
9852

9953

100-
def insert_header_in_hook(header: dict[str, str], lines: list[str]) -> str:
101-
"""Helper function to insert headers in hook's text."""
102-
for executable, header_text in header.items():
103-
if executable in lines[0].lower():
104-
lines.insert(1, dedent(header_text))
105-
return "\n".join(lines)
106-
return "\n".join(lines)
107-
108-
10954
@nox.session(name="pre-commit", python=python_versions[-1])
11055
def precommit(session: nox.Session) -> None:
11156
"""Lint using pre-commit."""
57+
install_with_uv(session, only_groups=["dev"])
11258
args = session.posargs or [
11359
"run",
11460
"--all-files",
11561
"--hook-stage=manual",
11662
"--show-diff-on-failure",
11763
]
118-
session.install(
119-
"pre-commit",
120-
"pre-commit-hooks",
121-
)
12264
session.run("pre-commit", *args)
123-
if args and args[0] == "install":
124-
activate_virtualenv_in_precommit_hooks(session)
12565

12666

127-
@nox.session(python=python_versions[-2:])
67+
@nox.session(python=python_versions)
12868
def mypy(session: nox.Session) -> None:
12969
"""Type-check using mypy."""
70+
install_with_uv(session, groups=["type_check", "test"])
13071
args = session.posargs or ["src", "tests"]
131-
session.install(".")
132-
session.install(
133-
"mypy",
134-
"pytest",
135-
"types-setuptools",
136-
"pandas-stubs",
137-
"pyarrow-stubs",
138-
"types-Pygments",
139-
"types-colorama",
140-
"types-beautifulsoup4",
141-
"faker",
142-
"tomli"
143-
)
14472
session.run("mypy", *args)
14573
if not session.posargs:
14674
session.run("mypy", f"--python-executable={sys.executable}", "noxfile.py")
14775

14876

149-
@nox.session(python=python_versions[-2:])
77+
@nox.session(python=python_versions)
15078
def tests(session: nox.Session) -> None:
15179
"""Run the test suite."""
152-
session.install(".")
153-
session.install(
154-
"coverage[toml]", "pytest", "pygments", "pytest-mock", "requests-mock", "faker", "tomli"
155-
)
80+
install_with_uv(session, groups=["test"])
15681
try:
15782
session.run(
15883
"coverage",
@@ -172,10 +97,8 @@ def tests(session: nox.Session) -> None:
17297
@nox.session(python=python_versions[-1])
17398
def coverage(session: nox.Session) -> None:
17499
"""Produce the coverage report."""
100+
install_with_uv(session, only_groups=["test"])
175101
args = session.posargs or ["report", "--skip-empty"]
176-
177-
session.install("coverage[toml]")
178-
179102
if not session.posargs and any(Path().glob(".coverage.*")):
180103
session.run("coverage", "combine")
181104

@@ -185,40 +108,30 @@ def coverage(session: nox.Session) -> None:
185108
@nox.session(python=python_versions[-1])
186109
def typeguard(session: nox.Session) -> None:
187110
"""Runtime type checking using Typeguard."""
188-
session.install(".")
189-
session.install(
190-
"pytest", "typeguard", "pygments", "pytest_mock", "requests_mock", "faker", "tomli"
191-
)
111+
install_with_uv(session, groups=["test"])
192112
session.run("pytest", f"--typeguard-packages={package}", *session.posargs)
193113

194114

195115
@nox.session(python=python_versions[-1])
196116
def xdoctest(session: nox.Session) -> None:
197117
"""Run examples with xdoctest."""
118+
install_with_uv(session, groups=["test"])
198119
if session.posargs:
199120
args = [package, *session.posargs]
200121
else:
201122
args = [f"--modname={package}", "--command=all"]
202123
if "FORCE_COLOR" in os.environ:
203124
args.append("--colored=1")
204-
205-
session.install(".")
206-
session.install("xdoctest[colors]")
207125
session.run("python", "-m", "xdoctest", *args)
208126

209127

210128
@nox.session(name="docs-build", python=python_versions[-1])
211129
def docs_build(session: nox.Session) -> None:
212130
"""Build the documentation."""
131+
install_with_uv(session, groups=["docs"])
213132
args = session.posargs or ["docs", "docs/_build"]
214133
if not session.posargs and "FORCE_COLOR" in os.environ:
215134
args.insert(0, "--color")
216-
217-
session.install(".")
218-
session.install(
219-
"sphinx", "sphinx-autodoc-typehints", "sphinx-click", "furo", "myst-parser"
220-
)
221-
222135
build_dir = Path("docs", "_build")
223136
if build_dir.exists():
224137
shutil.rmtree(build_dir)
@@ -229,17 +142,8 @@ def docs_build(session: nox.Session) -> None:
229142
@nox.session(python=python_versions[-1])
230143
def docs(session: nox.Session) -> None:
231144
"""Build and serve the documentation with live reloading on file changes."""
145+
install_with_uv(session, groups=["docs"])
232146
args = session.posargs or ["--open-browser", "docs", "docs/_build"]
233-
session.install(".")
234-
session.install(
235-
"sphinx",
236-
"sphinx-autobuild",
237-
"sphinx-autodoc-typehints",
238-
"sphinx-click",
239-
"furo",
240-
"myst-parser",
241-
)
242-
243147
build_dir = Path("docs", "_build")
244148
if build_dir.exists():
245149
shutil.rmtree(build_dir)

0 commit comments

Comments
 (0)