Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ jobs:
- name: Install the pinned version of uv
uses: astral-sh/setup-uv@v5
with:
python-version: 3.13
python-version: 3.12
pyproject-file: "${{ github.workspace }}/pyproject.toml"

- name: Install Nox
Expand Down
225 changes: 162 additions & 63 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Nox sessions."""

import os
import shlex
import shutil
import sys
from pathlib import Path
from tempfile import NamedTemporaryFile
from textwrap import dedent

import nox
from nox import Session
Expand All @@ -25,50 +27,147 @@
)


def session_install_uv(
session: Session,
install_project: bool = True,
install_dev: bool = False,
install_docs: bool = False,
) -> None:
"""Install root project into the session's virtual environment using uv."""
env = os.environ.copy()
env.update(
{
"UV_PROJECT_ENVIRONMENT": session.virtualenv.location,
"UV_PYTHON": sys.executable, # Force uv to use the session's interpreter
}
)

args = ["uv", "sync", "--frozen"]
if not install_project:
args.append("--no-install-project")
if not install_dev:
args.append("--no-dev")
if install_docs:
args.extend(["--group", "docs"])

session.run_install(*args, silent=True, env=env)


def session_install_uv_package(session: Session, packages: list[str]) -> None:
"""Install packages into the session's virtual environment using uv lockfile."""
env = os.environ.copy()
env.update(
{
"UV_PROJECT_ENVIRONMENT": session.virtualenv.location,
"UV_PYTHON": sys.executable,
}
)

# Export requirements.txt to session temp dir using uv with locked dependencies
requirements_tmp = str(Path(session.create_tmp()) / "requirements.txt")
export_args = ["uv", "export", "--only-dev", "--no-hashes", "-o", requirements_tmp]
session.run_install(*export_args, silent=True, env=env)

# Install requested packages with requirements.txt constraints
session.install(*packages, "--constraint", requirements_tmp)


def activate_virtualenv_in_precommit_hooks(session: Session) -> None:
"""Activate virtualenv in hooks installed by pre-commit.

This function patches git hooks installed by pre-commit to activate the
session's virtual environment. This allows pre-commit to locate hooks in
that environment when invoked from git.

Args:
session: The Session object.
"""
assert session.bin is not None # noqa: S101

# Only patch hooks containing a reference to this session's bindir. Support
# quoting rules for Python and bash, but strip the outermost quotes so we
# can detect paths within the bindir, like <bindir>/python.
bindirs = [
bindir[1:-1] if bindir[0] in "'\"" else bindir
for bindir in (repr(session.bin), shlex.quote(session.bin))
]

virtualenv = session.env.get("VIRTUAL_ENV")
if virtualenv is None:
return

headers = {
# pre-commit < 2.16.0
"python": f"""\
import os
os.environ["VIRTUAL_ENV"] = {virtualenv!r}
os.environ["PATH"] = os.pathsep.join((
{session.bin!r},
os.environ.get("PATH", ""),
))
""",
# pre-commit >= 2.16.0
"bash": f"""\
VIRTUAL_ENV={shlex.quote(virtualenv)}
PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH"
""",
# pre-commit >= 2.17.0 on Windows forces sh shebang
"/bin/sh": f"""\
VIRTUAL_ENV={shlex.quote(virtualenv)}
PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH"
""",
}

hookdir = Path(".git") / "hooks"
if not hookdir.is_dir():
return

for hook in hookdir.iterdir():
if hook.name.endswith(".sample") or not hook.is_file():
continue

if not hook.read_bytes().startswith(b"#!"):
continue

text = hook.read_text()

if not any(
Path("A") == Path("a") and bindir.lower() in text.lower() or bindir in text
for bindir in bindirs
):
continue

lines = text.splitlines()

for executable, header in headers.items():
if executable in lines[0].lower():
lines.insert(1, dedent(header))
hook.write_text("\n".join(lines))
break


@session(name="pre-commit", python=python_versions[0])
def precommit(session: Session) -> None:
"""Lint using pre-commit."""
session_install_uv(session, install_dev=True)
args = session.posargs or [
"run",
"--all-files",
"--hook-stage=manual",
"--show-diff-on-failure",
]
session.run(
"uv",
"pip",
"install",
"black",
"darglint",
"flake8",
"flake8-bandit",
"flake8-bugbear",
"flake8-docstrings",
"flake8-rst-docstrings",
"isort",
"pep8-naming",
"pre-commit",
"pre-commit-hooks",
"pyupgrade",
external=True,
)
session_install_uv_package(session, ["pre-commit"])
session.run("pre-commit", *args)
if args and args[0] == "install":
activate_virtualenv_in_precommit_hooks(session)


@session(python=python_versions[0])
def safety(session: Session) -> None:
"""Scan dependencies for insecure packages."""
with NamedTemporaryFile(delete=False) as requirements:
session.run(
"uv",
"pip",
"compile",
"pyproject.toml",
"--output-file",
requirements.name,
external=True,
)
session.run("uv", "pip", "install", "safety", external=True)
session_install_uv(session, install_dev=True)
session_install_uv_package(session, ["safety"])
# TODO(Altay): Remove the CVE ignore once its resolved.
# It's not critical, so ignoring now.
ignore = ["70612"]
Expand All @@ -88,8 +187,8 @@ def safety(session: Session) -> None:
def mypy(session: Session) -> None:
"""Type-check using mypy."""
args = session.posargs or ["src", "tests", "docs/conf.py"]
session.run_always("uv", "pip", "install", "-e", ".", external=True)
session.run("uv", "pip", "install", "mypy", "pytest", external=True)
session_install_uv(session, install_dev=True)
session_install_uv_package(session, ["mypy", "pytest"])
session.run("mypy", *args)
if not session.posargs:
session.run("mypy", f"--python-executable={sys.executable}", "noxfile.py")
Expand All @@ -98,7 +197,10 @@ def mypy(session: Session) -> None:
@session(python=python_versions)
def tests(session: Session) -> None:
"""Run the test suite."""
session.run_always("uv", "pip", "install", "-e", ".[cloud]", external=True)
session_install_uv(session, install_project=True, install_dev=True)
session_install_uv_package(
session, ["coverage", "pytest", "pygments", "pytest-dependency", "s3fs"]
)
session.run(
"uv",
"pip",
Expand All @@ -122,7 +224,8 @@ def coverage(session: Session) -> None:
"""Produce the coverage report."""
args = session.posargs or ["report"]

session.run("uv", "pip", "install", "coverage[toml]", external=True)
session_install_uv(session, install_project=True, install_dev=True)
session_install_uv_package(session, ["coverage"])

if not session.posargs and any(Path().glob(".coverage.*")):
session.run("coverage", "combine", external=True)
Expand All @@ -133,10 +236,8 @@ def coverage(session: Session) -> None:
@session(python=python_versions[0])
def typeguard(session: Session) -> None:
"""Runtime type checking using Typeguard."""
session.run_always("uv", "pip", "install", "-e", ".", external=True)
session.run(
"uv", "pip", "install", "pytest", "typeguard", "pygments", external=True
)
session_install_uv(session, install_project=True, install_dev=True)
session_install_uv_package(session, ["pytest", "typeguard", "pygments"])
session.run("pytest", f"--typeguard-packages={package}", *session.posargs)


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

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


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

session.run_always("uv", "pip", "install", "-e", ".", external=True)
session.run(
"uv",
"pip",
"install",
"sphinx",
"sphinx-click",
"sphinx-copybutton",
"furo",
"myst-nb",
"linkify-it-py",
external=True,
session_install_uv(session, install_project=True, install_dev=True)
session_install_uv_package(
session,
[
"sphinx",
"sphinx-click",
"sphinx-copybutton",
"furo",
"myst-nb",
"linkify-it-py",
],
)

build_dir = Path("docs", "_build")
Expand All @@ -187,19 +287,18 @@ def docs_build(session: Session) -> None:
def docs(session: Session) -> None:
"""Build and serve the documentation with live reloading on file changes."""
args = session.posargs or ["--open-browser", "docs", "docs/_build"]
session.run_always("uv", "pip", "install", "-e", ".", external=True)
session.run(
"uv",
"pip",
"install",
"sphinx",
"sphinx-autobuild",
"sphinx-click",
"sphinx-copybutton",
"furo",
"myst-nb",
"linkify-it-py",
external=True,
session_install_uv(session, install_project=True, install_dev=True)
session_install_uv_package(
session,
[
"sphinx",
"sphinx-autobuild",
"sphinx-click",
"sphinx-copybutton",
"furo",
"myst-nb",
"linkify-it-py",
],
)

build_dir = Path("docs", "_build")
Expand Down