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
9 changes: 0 additions & 9 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,6 @@ updates:
github-action-dependencies:
patterns:
- "*"
- package-ecosystem: "pip"
directory: "/.github/workflows"
schedule:
interval: "monthly"
open-pull-requests-limit: 99
groups:
workflows-dependencies:
patterns:
- "*"
- package-ecosystem: "uv"
directory: "/"
schedule:
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/constraints.txt

This file was deleted.

43 changes: 21 additions & 22 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,28 @@ on:
- ".pre-commit-config.yaml"
- "noxfile.py"
- ".github/workflows/tests.yml"
permissions:
contents: read

jobs:
generate-jobs:
runs-on: ubuntu-latest
outputs:
session: ${{ steps.set-matrix.outputs.session }}
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
- id: set-matrix
run: echo session=$(uvx nox@2025.11.12 --json -l | jq -c '[.[].session]') | tee --append $GITHUB_OUTPUT
tests:
name: ${{ matrix.session }} ${{ matrix.python }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
name: ${{ matrix.session }}
needs: [generate-jobs]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- { python: "3.13", os: "ubuntu-latest", session: "pre-commit" }
- { python: "3.12", os: "ubuntu-latest", session: "mypy" }
- { python: "3.13", os: "ubuntu-latest", session: "mypy" }
- { python: "3.12", os: "ubuntu-latest", session: "tests" }
- { python: "3.13", os: "ubuntu-latest", session: "tests" }
- { python: "3.13", os: "ubuntu-latest", session: "typeguard" }
- { python: "3.13", os: "ubuntu-latest", session: "xdoctest" }
- { python: "3.13", os: "ubuntu-latest", session: "docs-build" }
session: ${{ fromJson(needs.generate-jobs.outputs.session) }}

env:
NOXSESSION: ${{ matrix.session }}
Expand All @@ -39,12 +44,6 @@ jobs:
steps:
- name: Check out the repository
uses: actions/checkout@v6

- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v6.1.0
with:
python-version: ${{ matrix.python }}

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
Expand Down Expand Up @@ -77,13 +76,13 @@ jobs:

- name: Run Nox
run: |
uvx --constraints "${{ github.workspace }}/.github/workflows/constraints.txt" nox --python=${{ matrix.python }}
uvx nox@2025.11.12 --session=${{ matrix.session }}

- name: Upload coverage data
if: always() && matrix.session == 'tests'
if: always() && contains(matrix.session, 'tests')
uses: "actions/upload-artifact@v6"
with:
name: coverage-data-${{ matrix.os }}-${{ matrix.python }}
name: coverage-data-${{ matrix.session }}
path: ".coverage.*"
include-hidden-files: true

Expand Down Expand Up @@ -116,11 +115,11 @@ jobs:

- name: Combine coverage data and display human readable report
run: |
uvx --constraints "${{ github.workspace }}/.github/workflows/constraints.txt" nox --session=coverage
uvx nox@2025.11.12 --reuse-venv=yes --session=coverage

- name: Create coverage report
run: |
uvx --constraints "${{ github.workspace }}/.github/workflows/constraints.txt" nox --session=coverage -- xml
uvx nox@2025.11.12 --reuse-venv=yes --session=coverage -- xml

# Need to fix coverage source paths for SonarCloud scanning in GitHub actions.
# Replace root path with /github/workspace (mounted in docker container).
Expand Down
168 changes: 36 additions & 132 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import nox

package = "datadoc_editor"
python_versions = ["3.12", "3.13"]
python_versions = ["3.12", "3.13", "3.14"]
nox.needs_version = ">= 2021.6.6"
nox.options.default_venv_backend = "uv"
nox.options.sessions = (
Expand All @@ -23,136 +23,61 @@
)


def activate_virtualenv_in_precommit_hooks(session: nox.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 # nosec

# 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 is_bindir_in_text(bindirs, text):
continue

lines = text.splitlines()
hook.write_text(insert_header_in_hook(headers, lines))


def is_bindir_in_text(bindirs: list[str], text: str) -> bool:
"""Helper function to check if bindir is in text."""
return any(
Path("A") == Path("a") and bindir.lower() in text.lower() or bindir in text
for bindir in bindirs
def install_with_uv(
session: nox.Session,
*,
groups: list[str] | None = None,
only_groups: list[str] | None = None,
all_extras: bool = False,
locked: bool = True,
) -> None:
"""Install packages using uv, pinned to uv.lock."""
cmd = ["uv", "sync", "--no-default-groups"]
if locked:
cmd.append("--locked")
if groups:
for group in groups:
cmd.extend(["--group", group])
if only_groups:
for group in only_groups or []:
cmd.extend(["--only-group", group])
if all_extras:
cmd.append("--all-extras")
cmd.append(
f"--python={session.virtualenv.location}"
) # Target the nox venv's Python interpreter
session.run_install(
*cmd, env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}
)


def insert_header_in_hook(header: dict[str, str], lines: list[str]) -> str:
"""Helper function to insert headers in hook's text."""
for executable, header_text in header.items():
if executable in lines[0].lower():
lines.insert(1, dedent(header_text))
return "\n".join(lines)
return "\n".join(lines)


@nox.session(name="pre-commit", python=python_versions[-1])
def precommit(session: nox.Session) -> None:
"""Lint using pre-commit."""
install_with_uv(session, only_groups=["dev"])
args = session.posargs or [
"run",
"--all-files",
"--hook-stage=manual",
"--show-diff-on-failure",
]
session.install(
"pre-commit",
"pre-commit-hooks",
)
session.run("pre-commit", *args)
if args and args[0] == "install":
activate_virtualenv_in_precommit_hooks(session)


@nox.session(python=python_versions[-2:])
@nox.session(python=python_versions)
def mypy(session: nox.Session) -> None:
"""Type-check using mypy."""
install_with_uv(session, groups=["type_check", "test"])
args = session.posargs or ["src", "tests"]
session.install(".")
session.install(
"mypy",
"pytest",
"types-setuptools",
"pandas-stubs",
"pyarrow-stubs",
"types-Pygments",
"types-colorama",
"types-beautifulsoup4",
"faker",
"tomli"
)
session.run("mypy", *args)
if not session.posargs:
session.run("mypy", f"--python-executable={sys.executable}", "noxfile.py")


@nox.session(python=python_versions[-2:])
@nox.session(python=python_versions)
def tests(session: nox.Session) -> None:
"""Run the test suite."""
session.install(".")
session.install(
"coverage[toml]", "pytest", "pygments", "pytest-mock", "requests-mock", "faker", "tomli"
)
install_with_uv(session, groups=["test"])
try:
session.run(
"coverage",
Expand All @@ -172,10 +97,8 @@ def tests(session: nox.Session) -> None:
@nox.session(python=python_versions[-1])
def coverage(session: nox.Session) -> None:
"""Produce the coverage report."""
install_with_uv(session, only_groups=["test"])
args = session.posargs or ["report", "--skip-empty"]

session.install("coverage[toml]")

if not session.posargs and any(Path().glob(".coverage.*")):
session.run("coverage", "combine")

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


@nox.session(python=python_versions[-1])
def xdoctest(session: nox.Session) -> None:
"""Run examples with xdoctest."""
install_with_uv(session, groups=["test"])
if session.posargs:
args = [package, *session.posargs]
else:
args = [f"--modname={package}", "--command=all"]
if "FORCE_COLOR" in os.environ:
args.append("--colored=1")

session.install(".")
session.install("xdoctest[colors]")
session.run("python", "-m", "xdoctest", *args)


@nox.session(name="docs-build", python=python_versions[-1])
def docs_build(session: nox.Session) -> None:
"""Build the documentation."""
install_with_uv(session, groups=["docs"])
args = session.posargs or ["docs", "docs/_build"]
if not session.posargs and "FORCE_COLOR" in os.environ:
args.insert(0, "--color")

session.install(".")
session.install(
"sphinx", "sphinx-autodoc-typehints", "sphinx-click", "furo", "myst-parser"
)

build_dir = Path("docs", "_build")
if build_dir.exists():
shutil.rmtree(build_dir)
Expand All @@ -229,17 +142,8 @@ def docs_build(session: nox.Session) -> None:
@nox.session(python=python_versions[-1])
def docs(session: nox.Session) -> None:
"""Build and serve the documentation with live reloading on file changes."""
install_with_uv(session, groups=["docs"])
args = session.posargs or ["--open-browser", "docs", "docs/_build"]
session.install(".")
session.install(
"sphinx",
"sphinx-autobuild",
"sphinx-autodoc-typehints",
"sphinx-click",
"furo",
"myst-parser",
)

build_dir = Path("docs", "_build")
if build_dir.exists():
shutil.rmtree(build_dir)
Expand Down
Loading