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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ updates:
update-types: ["minor", "patch"]
ignore:
# 1.1.0 pins `syrupy==4.8.0`, which caps `pytest<9` and drags pytest
# back to 8.4.2 — PYSEC-2026-1845. Drop this once it allows syrupy 5.
# back to 8.4.2: PYSEC-2026-1845. Drop this once it allows syrupy 5.
- dependency-name: "pytest-textual-snapshot"
versions: ["1.1.0"]
open-pull-requests-limit: 5
10 changes: 5 additions & 5 deletions .github/scripts/aggregate_junit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Reads the junit reports one per (OS, run) and prints a markdown summary.
A test that fails in some runs and passes in others is a flake; one that
fails in every run of an OS is a real, platform-specific failure — the
fails in every run of an OS is a real, platform-specific failure: the
distinction the workflow exists to make.
"""

Expand Down Expand Up @@ -68,7 +68,7 @@ def main(root: Path, expect_oses: list[str] | None = None, expect_runs: int = 0)
detail.setdefault(nodeid, _detail(case))

total = sum(len(v) for v in runs.values())
out = [f"## Flake hunt — {total} suite runs\n"]
out = [f"## Flake hunt: {total} suite runs\n"]

# A job that dies before uploading leaves no directory at all, so a smaller
# count is the ONLY trace of it. Say so loudly: a table that silently
Expand All @@ -83,7 +83,7 @@ def main(root: Path, expect_oses: list[str] | None = None, expect_runs: int = 0)
if absent:
out.append(
f"> **{len(absent)} of {len(expect_oses) * expect_runs} suites reported "
f"nothing** — {', '.join(absent)}. Still running, or the job died before "
f"nothing**: {', '.join(absent)}. Still running, or the job died before "
"uploading. Everything below describes only the suites that reported.\n"
)
out.append("| OS | runs | runs with a failure |")
Expand All @@ -94,7 +94,7 @@ def main(root: Path, expect_oses: list[str] | None = None, expect_runs: int = 0)
out.append("")

if missing:
out.append(f"> No report from: {', '.join(missing)} — job died before pytest wrote one.\n")
out.append(f"> No report from: {', '.join(missing)}, job died before pytest wrote one.\n")

if not fails:
out.append("**Every run green.** No test failed in any run.")
Expand Down Expand Up @@ -129,7 +129,7 @@ def _json_list(arg: str) -> list[str]:


if __name__ == "__main__":
# argv: <reports-dir> [oses-json] [indices-json] — the matrix that was ASKED
# argv: <reports-dir> [oses-json] [indices-json]; the matrix that was ASKED
# for, so a suite that never reported is named rather than silently missing.
_dir = Path(sys.argv[1] if len(sys.argv) > 1 else "reports")
_oses = _json_list(sys.argv[2]) if len(sys.argv) > 2 else None
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/check_ruff_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def main() -> int:

if hook != locked:
print(
f"::error::ruff drift — .pre-commit-config.yaml pins v{hook}, uv.lock resolves {locked}"
f"::error::ruff drift, .pre-commit-config.yaml pins v{hook}, uv.lock resolves {locked}"
)
print(
"\nThe hook and CI must run the same ruff. Drift means the hook "
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/check_runtime_pins.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def main() -> int:
f"\n{len(bad)} user-facing dependency specifier(s) are not pinned.\n"
"uv.lock reaches no installer, so these specifiers are what every user\n"
"resolves. A widened range ships versions CI never ran.\n"
"Dependabot widens by default — correct the specifier before merging."
"Dependabot widens by default: correct the specifier before merging."
)
return 1

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
# --refresh so a cached wheel cannot mask a broken publish.
out=$(uvx --refresh --from "fndr==$VERSION" fnd version 2>&1)
printf '%s\n' "$out"
# Whole line, not substring — 0.0.50 contains 0.0.5.
# Whole line, not substring: 0.0.50 contains 0.0.5.
printf '%s\n' "$out" | grep -qFx "$VERSION" \
|| { echo "::error::PyPI install did not report exactly $VERSION"; exit 1; }

Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:
body="The weekly canary failed installing the published fndr \`$VERSION\`: $RUN_URL

This means users cannot install, or are installing the wrong version.
The run shows which channel — PyPI, Homebrew, or a specific OS.
The run shows which channel: PyPI, Homebrew, or a specific OS.

This is user-facing and outranks anything else in the queue."
existing=$(gh issue list --search "$title in:title" --state open \
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/deps-latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
echo "### Newest allowed vs committed uv.lock (${{ matrix.os }})"
echo '```diff'
if diff -u /tmp/before.txt /tmp/after.txt | tail -n +3 | grep -E '^[-+]'; then :; else
echo "(nothing moved — the lock is already at the newest allowed versions)"
echo "(nothing moved; the lock is already at the newest allowed versions)"
fi
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
Expand Down Expand Up @@ -90,7 +90,7 @@ jobs:
summary lists exactly which versions moved. Do not raise the
affected pin in \`pyproject.toml\` until this is green again.

Users are unaffected while the pins stay as they are — that is what
Users are unaffected while the pins stay as they are: that is what
the three-component \`~=\` ranges buy."
# Comment on the standing issue rather than opening a fresh one each
# Monday, so a long-lived upstream break is one thread, not twelve.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/flake-hunt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: flake-hunt

# Runs the suite N times per OS and reports which tests failed and how
# often. A single CI run cannot tell a flake from a fix; this can.
# Manual dispatch only — it costs N times a normal run.
# Manual dispatch only: it costs N times a normal run.

on:
# `workflow_dispatch` only fires for workflows already on the default
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/quarterly-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ jobs:
run: |
set -euo pipefail
gh issue create \
--title "Quarterly dependency review — $(date -u '+%Y-%m')" \
--title "Quarterly dependency review: $(date -u '+%Y-%m')" \
--body "Per SECURITY.md, review the security-critical parsers this quarter.

For each of **pymupdf, python-docx, python-pptx, tantivy, markdown-it-py**:

- [ ] Read the changelog since the pinned version for disclosed security fixes
- [ ] \`uv lock --upgrade-package <name>\`
- [ ] Raise the \`~=X.Y.Z\` pin in \`pyproject.toml\` to match — the pin, not the lock, is what users get
- [ ] Raise the \`~=X.Y.Z\` pin in \`pyproject.toml\` to match: the pin, not the lock, is what users get
- [ ] Confirm \`deps-latest\` and the full suite are green at the new version
- [ ] Ship as a patch release if anything security-relevant was disclosed

Expand All @@ -70,4 +70,4 @@ jobs:
\`\`\`

Note this diff only shows movement *allowed by the pins* (patches). A
new minor will not appear here — check the upstream changelogs directly."
new minor will not appear here: check the upstream changelogs directly."
22 changes: 11 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: release

# Triggered on any `v*` tag. Builds sdist + wheel, emits SBOM and a
# SLSA-style build-provenance attestation, publishes to PyPI via a
# Trusted Publisher (OIDC — no API token in repo secrets), and opens
# Trusted Publisher (OIDC: no API token in repo secrets), and opens
# a bump-PR against the Homebrew tap with the new sdist SHA-256.

on:
Expand Down Expand Up @@ -34,15 +34,15 @@ jobs:
with:
fetch-depth: 0

- name: Guard — release tag matches pyproject version
- name: Guard (release tag matches pyproject version)
# Fail fast (before build/publish) if the `vX.Y.Z` tag and the
# declared version have drifted. pyproject is the single source;
# this just stops a mismatched tag from shipping.
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME#v}"
ver=$(python3 -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
[ "$tag" = "$ver" ] || { echo "::error::tag v$tag != pyproject version $ver — bump pyproject.toml before tagging"; exit 1; }
[ "$tag" = "$ver" ] || { echo "::error::tag v$tag != pyproject version $ver, bump pyproject.toml before tagging"; exit 1; }
echo "tag and pyproject version agree: $ver"

- name: Install uv
Expand Down Expand Up @@ -82,7 +82,7 @@ jobs:
sha=$(shasum -a 256 "$sdist" | awk '{print $1}')
# PEP 625 sdist name is `{name}-{version}.tar.gz` and the name
# part carries no `-` (normalised to `_`), so the first `-`
# splits name from version — name-agnostic (fndr, fnd_x, …).
# splits name from version: name-agnostic (fndr, fnd_x, …).
base=$(basename "$sdist" .tar.gz)
ver="${base#*-}"
echo "sdist=$(basename "$sdist")" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -176,15 +176,15 @@ jobs:
env:
# HOMEBREW_TAP_PAT: fine-scoped PAT with PR-write to the tap
# (the default GITHUB_TOKEN can't push cross-repo). Both live
# on THIS repo, not the tap — the workflow runs here.
# on THIS repo, not the tap: the workflow runs here.
# HOMEBREW_TAP_REPO = ben-dev-au/homebrew-tap.
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_PAT }}
TAP_REPO: ${{ vars.HOMEBREW_TAP_REPO }}
VERSION: ${{ needs.build.outputs.version }}
if: ${{ vars.HOMEBREW_TAP_REPO != '' }}
run: |
set -euo pipefail
# Resolve url + sha256 from PyPI's JSON API — one source of
# Resolve url + sha256 from PyPI's JSON API: one source of
# truth, immune to name normalisation and the hashed storage
# path. The index can lag the publish by a few seconds, so retry.
meta=""
Expand Down Expand Up @@ -212,15 +212,15 @@ jobs:
existing_url=$(sed -nE 's/^ url "([^"]+)"/\1/p' "$formula" | head -n1)
existing_sha=$(sed -nE 's/^ sha256 "([^"]+)"/\1/p' "$formula" | head -n1)
if [ "$existing_url" = "$new_url" ] && [ "$existing_sha" = "$new_sha" ]; then
echo "Homebrew formula already at $VERSION — leaving its bottle block alone"
echo "Homebrew formula already at $VERSION, leaving its bottle block alone"
exit 0
fi
sed -i.bak "s|^ url \".*\"| url \"$new_url\"|" "$formula"
sed -i.bak "s|^ sha256 \".*\"| sha256 \"$new_sha\"|" "$formula"
# Drop the PREVIOUS version's `bottle do` block. Homebrew derives the
# bottle filename from the formula version, so a bumped url + a stale
# root_url makes every `brew upgrade fnd` 404 until the tap's bottle
# workflow runs. With no block, brew builds from source instead —
# workflow runs. With no block, brew builds from source instead:
# slower, but never broken. The bottle job re-adds it via
# `brew bottle --merge --write`. No-op when the block is absent.
sed -i.bak "/^ bottle do$/,/^ end$/d" "$formula"
Expand All @@ -233,7 +233,7 @@ jobs:
if git diff --cached --quiet; then
# Re-run for a version whose formula is already current: `git commit`
# would exit 1 on an empty commit and fail the job before the PR.
echo "Homebrew formula already at $VERSION — nothing to bump"
echo "Homebrew formula already at $VERSION: nothing to bump"
exit 0
fi
git -c user.email="actions@github.com" -c user.name="fnd-release-bot" \
Expand All @@ -244,7 +244,7 @@ jobs:
--body "Automated bump from ben-dev-au/fnd@${GITHUB_REF##*/}. sha256 \`$new_sha\`.")
# Merge it here rather than leaving it for a human. The diff is
# machine-generated from PyPI's own metadata and the tap runs no PR
# checks, so the manual gate bought nothing — and forgetting it is
# checks, so the manual gate bought nothing, and forgetting it is
# what left 0.0.5's formula serving a 404 for twelve hours. Merging
# is also what triggers the tap's bottle build.
gh pr merge "$pr" --squash --delete-branch
Expand Down Expand Up @@ -283,7 +283,7 @@ jobs:
# a bare grep.
[ "$installed" -eq 1 ] \
|| { echo "::error::fndr $VERSION never became installable"; exit 1; }
# Whole line, not substring — 0.0.50 contains 0.0.5.
# Whole line, not substring: 0.0.50 contains 0.0.5.
printf '%s\n' "$out" | grep -qFx "$VERSION" \
|| { echo "::error::\`fnd version\` did not report exactly $VERSION"; exit 1; }
echo "PyPI install OK: fndr $VERSION"
6 changes: 3 additions & 3 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
pull_request:
branches: [main]
schedule:
# Weekly Monday 09:00 UTC — catches CVEs published since the last
# Weekly Monday 09:00 UTC, catches CVEs published since the last
# human-driven push.
- cron: "0 9 * * 1"

Expand Down Expand Up @@ -36,8 +36,8 @@ jobs:
# `--no-emit-project` drops the local `fnd` package (which uv
# emits as `-e .`); pip-audit can't look up an unpublished
# editable install and would fail the whole audit. Once Phase
# 0 of the publish runbook lands `fnd` on PyPI, the flag stays
# — we don't need to audit ourselves, only our deps.
# 0 of the publish runbook lands `fnd` on PyPI, the flag stays:
# we don't need to audit ourselves, only our deps.
run: |
uv export \
--format requirements-txt \
Expand Down
2 changes: 1 addition & 1 deletion .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
# scans the full commit range, not just HEAD.
# 2. The note above used to quote that fixture verbatim, so this file
# tripped the very rule it suppresses. Reworded, but the original
# commit is immutable — hence the second fingerprint.
# commit is immutable, hence the second fingerprint.
ac26e70dc720c7a3b44a5a09497a395814514b4f:tests/test_extraction_cache.py:generic-api-key:84
113b88e9f185faf64456691a7e90d860862d232a:.gitleaksignore:generic-api-key:1
10 changes: 5 additions & 5 deletions fnd/_perms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
By default ``Path.mkdir(parents=True)`` honours the process umask
(0o022 on macOS), which leaves directories world-readable. fnd's
``config.toml`` and on-disk state contain absolute paths to the user's
private documents — readable by any other local account on a shared
private documents: readable by any other local account on a shared
Mac. These helpers normalise everything we create to 0o700 dirs and
0o600 files so the published binary doesn't bake in the "single-user
Mac" assumption.
Expand All @@ -16,7 +16,7 @@

Platform note: POSIX modes are only meaningful on macOS/Linux. On Windows
``os.chmod`` merely toggles the read-only bit (the 0o700/0o600 modes are a
no-op there), so this hardening is a POSIX-only guarantee — but the Windows
no-op there), so this hardening is a POSIX-only guarantee, but the Windows
per-user profile root (``%LOCALAPPDATA%``) is already ACL-scoped to its
owner, which provides the equivalent protection."""

Expand All @@ -31,7 +31,7 @@
def _chmod_quiet(p: Path, mode: int) -> None:
"""Best-effort chmod. Some filesystems (smbfs, exfat, some external
drives) don't honour POSIX modes; swallowing the error is the right
call — we tried."""
call: we tried."""
with contextlib.suppress(OSError):
os.chmod(p, mode)

Expand All @@ -43,7 +43,7 @@ def secure_mkdir(path: Path, *, anchor: Path | None = None) -> Path:
Returns ``path`` so callers can chain.
"""
if anchor is None:
# Lazy import — ``fnd.config`` imports back from this module.
# Lazy import: ``fnd.config`` imports back from this module.
from fnd.config import app_data_dir

anchor = app_data_dir()
Expand All @@ -53,7 +53,7 @@ def secure_mkdir(path: Path, *, anchor: Path | None = None) -> Path:
try:
rel = path.relative_to(anchor)
except ValueError:
# Caller asked for a dir outside our anchor — only chmod the leaf.
# Caller asked for a dir outside our anchor: only chmod the leaf.
_chmod_quiet(path, 0o700)
return path
cur = anchor
Expand Down
Loading
Loading