diff --git a/.claude/hooks/format_python.py b/.claude/hooks/format_python.py index efa0e47..c429b16 100755 --- a/.claude/hooks/format_python.py +++ b/.claude/hooks/format_python.py @@ -1,4 +1,9 @@ #!/usr/bin/env python3 +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """PostToolUse hook: format an edited Python file with the locked ruff. Only formatting and import sorting run here. Other autofixes would delete diff --git a/.claude/hooks/stop_gate.py b/.claude/hooks/stop_gate.py index 09603cf..222ebbe 100755 --- a/.claude/hooks/stop_gate.py +++ b/.claude/hooks/stop_gate.py @@ -1,4 +1,9 @@ #!/usr/bin/env python3 +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Stop hook: when Python files changed, run `make lint type` before stopping. On failure the output goes back to the agent (exit 2) so it fixes the diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e541c0e..b9af093 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,28 @@ jobs: - name: Examples and notebooks run: make smoke + windows: + name: Windows correctness (Python ${{ matrix.python-version }}) + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.14"] + env: + CI: "1" + steps: + - name: Preserve repository symlinks + run: git config --global core.symlinks true + - uses: actions/checkout@v7 + - uses: astral-sh/setup-uv@v10.2.0 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + - name: Install locked dependencies + run: uv sync --locked + - name: Correctness tests + run: uv run --frozen pytest -m "not slow" -ra + lowest: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 906ee2e..20542a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,25 +5,61 @@ on: tags: ["v*"] permissions: - contents: write + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false jobs: - release: + build: runs-on: ubuntu-latest + outputs: + prerelease: ${{ steps.version.outputs.prerelease }} steps: - uses: actions/checkout@v7 + with: + fetch-depth: 0 - uses: astral-sh/setup-uv@v10.2.0 with: enable-cache: true - name: Install run: uv sync --locked + - name: Validate release + run: uv run --frozen python scripts/release.py --tag "$GITHUB_REF_NAME" + - name: Determine release kind + id: version + run: echo "prerelease=$(uv run --frozen python scripts/release.py --tag "$GITHUB_REF_NAME" --prerelease)" >> "$GITHUB_OUTPUT" - name: Check (lint + types + tests + coverage) run: make check env: CI: "1" + - name: Recheck cleanliness + run: uv run --frozen python scripts/release.py --check-clean - name: Build sdist and wheel - run: uv build + run: uv build --no-sources + - name: Smoke-test installed artifacts + run: make package-smoke VERSION="${GITHUB_REF_NAME#v}" + - uses: actions/upload-artifact@v7 + with: + name: distributions + path: dist/* + if-no-files-found: error + publish: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v8 + with: + name: distributions + path: dist - name: GitHub release - run: gh release create "$GITHUB_REF_NAME" dist/* --title "$GITHUB_REF_NAME" --generate-notes --verify-tag + run: | + flags=() + if [ "$PRERELEASE" = true ]; then flags+=(--prerelease); fi + gh release create "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" --title "$GITHUB_REF_NAME" --generate-notes --verify-tag "${flags[@]}" env: GH_TOKEN: ${{ github.token }} + PRERELEASE: ${{ needs.build.outputs.prerelease }} diff --git a/.license-header.txt b/.license-header.txt new file mode 100644 index 0000000..fdec29c --- /dev/null +++ b/.license-header.txt @@ -0,0 +1,4 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 000846e..b55b6e7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ # default_stages keeps the file-filtered hooks above from running at the -# commit-msg stage, where pre-commit would otherwise feed them +# commit-msg stage, where the hook runner would otherwise feed them # .git/COMMIT_EDITMSG; default_install_hook_types makes -# `pre-commit install` install both hook types. +# `prek install` install both hook types. Git stage names stay unchanged. default_install_hook_types: [pre-commit, commit-msg] default_stages: [pre-commit] repos: @@ -18,6 +18,12 @@ repos: # never disagree on the ruff version - repo: local hooks: + - id: license-headers + name: canonical license headers + entry: make headers + language: system + pass_filenames: false + always_run: true - id: cleanporter-fix name: cleanporter fix entry: uv run --frozen cleanporter --fix --whole-project diff --git a/AGENTS.md b/AGENTS.md index dfe6d32..81a9a73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ Invariants: The code follows the Google Python Style Guide, single deviation: 100-column lines. Imports follow Google §2.2 (import modules, not names), enforced by cleanporter in `make lint` and `make fmt`; ruff -enforces the rest via `make lint` / pre-commit. +enforces the rest via `make lint` / prek hooks. ## Tests @@ -105,6 +105,12 @@ then link it for Claude: ## Docs and commits +- Python and stub files, including golden snapshots, notebooks, and hidden scripts, + carry the canonical `.license-header.txt` notice. `make headers` checks the entire + Git-owned tree plus nonignored untracked files; `make headers-fix` applies it. + Never replace conflicting ownership or license notices. Golden writers add the + header only to snapshots, not to generated function source. + - `docs/` is the single source for the supported subset, semantics, divergences, and API. `README.md` only links to it. A user-visible change updates the matching page in the same commit; moving modules diff --git a/Makefile b/Makefile index 00ff4e8..0c8337d 100644 --- a/Makefile +++ b/Makefile @@ -12,11 +12,20 @@ export UV_PROJECT_ENVIRONMENT := .venv-$(PY) endif PY_PATHS := src tests scripts examples .claude/hooks -.PHONY: install fmt lint type type-mypy type-pyright type-zuban type-pyrefly test coverage check lowest smoke fuzz bench backends docs docs-serve notebook release +.PHONY: install hooks headers headers-fix fmt lint type type-mypy type-pyright type-zuban type-pyrefly test coverage check lowest smoke fuzz bench backends docs docs-serve notebook release package-smoke install: $(UV) sync - $(RUN) pre-commit install + $(RUN) --frozen prek install --force + +hooks: + $(RUN) --frozen prek run --all-files --show-diff-on-failure + +headers: + $(RUN) --frozen python scripts/license_headers.py --check + +headers-fix: + $(RUN) --frozen python scripts/license_headers.py --fix # fixers run to completion whatever findings remain; `make lint` stays # the gate. cleanporter gets `test $$? -le 1`: it exits 1 whenever @@ -25,13 +34,13 @@ install: # gets --exit-zero: ruff cannot fix E501 itself, and cleanporter's # module prefixes push lines past 100 columns; ruff format runs first # and last so the fixes land on wrapped code -fmt: +fmt: headers-fix $(RUN) cleanporter --fix $(PY_PATHS); test $$? -le 1 $(RUN) ruff format $(PY_PATHS) $(RUN) ruff check --fix --exit-zero $(PY_PATHS) $(RUN) ruff format $(PY_PATHS) -lint: +lint: headers $(RUN) ruff check $(PY_PATHS) $(RUN) ruff format --check $(PY_PATHS) # the import gate: exits 0 only when clean -- treat_unresolved_as_error @@ -68,11 +77,15 @@ check: lint type coverage # git tag -a v$(VERSION) -m "v$(VERSION)" release: @test "$(VERSION)" != "" || { echo "usage: make release VERSION=0.1.0" >&2; exit 1; } - @grep -qx 'version = "$(VERSION)"' pyproject.toml || { echo "pyproject.toml is not at version $(VERSION)" >&2; exit 1; } - @grep -qx '__version__ = "$(VERSION)"' src/array_vectorize/__init__.py || { echo "src/array_vectorize/__init__.py is not at version $(VERSION)" >&2; exit 1; } - $(RUN) uv lock --check - $(MAKE) check - $(RUN) uv build + $(RUN) --frozen python scripts/release.py --tag "v$(VERSION)" + CI=1 $(MAKE) check + $(RUN) --frozen python scripts/release.py --check-clean + $(UV) build --no-sources + $(MAKE) package-smoke VERSION="$(VERSION)" + +package-smoke: + @test "$(VERSION)" != "" || { echo "usage: make package-smoke VERSION=0.1.0" >&2; exit 1; } + $(RUN) --frozen python scripts/package_smoke.py --version "$(VERSION)" # the test suite with every direct dependency at the minimum pyproject.toml # allows, on the minimum Python; proves the declared floors diff --git a/docs/api.md b/docs/api.md index 0070952..13717ac 100644 --- a/docs/api.md +++ b/docs/api.md @@ -91,3 +91,10 @@ python -m array_vectorize.fuzz --seconds 60 --seed 0 Runs the grammar fuzzer (differential checks of random scalar programs). Reproducible with `--seed`. + +| Option | Meaning | +|---|---| +| `--seconds` | time budget in seconds (default: 60) | +| `--seed` | random seed for a reproducible run | +| `--cases` | maximum cases; overrides the time budget | +| `--help` | show usage and exit | diff --git a/docs/development.md b/docs/development.md index 4c091e9..0b54850 100644 --- a/docs/development.md +++ b/docs/development.md @@ -8,7 +8,7 @@ are locked in `uv.lock`, the development Python version is pinned in virtualenv needs to be activated. ```bash -make install # uv sync (dev group) + pre-commit hooks +make install # uv sync (dev group) + prek hooks ``` Optional dependency groups: `backends` (jax, CPU torch), `docs` @@ -22,13 +22,16 @@ dependency at its floor (`--resolution lowest-direct`) on Python 3.12. ## Commands ```bash +make hooks # all Git-tracked files through prek, with diffs on failure make check # cleanporter + ruff + four type checkers + pytest + coverage (95%) make check PY=3.13 # any target on another Python (env: .venv-3.13) make lint type test # the same gates without coverage make type # mypy + pyright + zuban + pyrefly make type-mypy # individual checker (also: type-pyright, type-zuban, type-pyrefly) make lowest # the test suite at the minimum dependency versions -make fmt # cleanporter --fix, then ruff format / check --fix / format +make fmt # headers, cleanporter --fix, then ruff format / check --fix / format +make headers # check canonical headers, including untracked Python files +make headers-fix # apply canonical headers without changing source bodies make smoke # run scripts/smoke.py, examples/demo.py, the notebooks make fuzz # grammar fuzzer on the CI seeds make bench # pytest-benchmark suite (timings + dispatch gates) @@ -36,7 +39,8 @@ make backends # jax.jit / torch.compile compatibility tests make docs # build this site (zensical, --strict) make docs-serve # live-reload preview make notebook # open the marimo example notebooks -make release VERSION=0.1.0 # verify + gate + build dist/ for a release +make release VERSION=0.1.0 # validate + gate + build + installed artifact checks +make package-smoke VERSION=0.1.0 # test wheel and sdist already in dist/ uv run pytest --update-golden # regenerate golden source snapshots uv run python -m array_vectorize.fuzz --seconds 60 --seed 0 # fuzz longer ``` @@ -60,10 +64,10 @@ the Makefile, hook configuration, and dependency/typing configuration trigger th | Checker | Scope | Mode | |---|---|---| -| mypy | `src` | strict | -| Pyright | `src` | standard | -| Zuban | `src` and maintained tests | strict, with test annotation requirements relaxed | -| Pyrefly | `src` and maintained tests | default diagnostics, including unannotated bodies | +| mypy | `src` and `scripts` | strict | +| Pyright | `src` and `scripts` | standard | +| Zuban | `src`, `scripts`, and maintained tests | strict, with test annotation requirements relaxed | +| Pyrefly | `src`, `scripts`, and maintained tests | default diagnostics, including unannotated bodies | All target Python 3.12. Zuban has its own configuration rather than inheriting mypy's source-only scope. The wider pair resolves `support` and `corpus` from @@ -112,6 +116,17 @@ numeric warnings (`overflow encountered`, `invalid value`, ## Style +Repository-owned Python and stub files carry the canonical template in +`.license-header.txt`, followed by one blank line. This includes golden snapshots, +marimo notebooks, tests, and hidden scripts. The notice identifies grAItools, +2026, and BSD-3-Clause; its year is updated centrally rather than per file. +`make headers` checks tracked and nonignored untracked files without writing; +`make headers-fix` applies the template, preserving shebangs, encoding declarations, +line endings, and source bodies. Conflicting ownership or license notices and source +symlinks are refused. Ignored environments, downloaded code, and embedded snippets +are outside the policy. The whole-tree hook runs even on template-only commits. +Golden snapshot writers preserve the header; generated function source is unchanged. + The code follows the [Google Python Style Guide] (https://google.github.io/styleguide/pyguide.html). Ruff enforces it in `make lint`: pydocstyle with the google convention, pep8-naming, import @@ -145,6 +160,10 @@ the NumPy-style docstring subject). - **static** — `make lint type` once on Python 3.12, including all four checkers. - **check** — `make coverage` on Python 3.12–3.14 (Hypothesis derandomized), then `make smoke`. +- **windows** — correctness tests on Python 3.12 and 3.14 with locked dependencies + and the deterministic Hypothesis profile. Git symlink support is configured before + checkout so agent-layout checks remain enabled. Timing-sensitive performance tests + stay on Linux; optional JAX/PyTorch compilation stays in its dedicated job. - **lowest** — `make lowest`: the suite at the declared dependency floors. - **fuzz** — the five fixed fuzzer seeds. - **compile** — proves the generated source traces correctly under @@ -156,9 +175,13 @@ the NumPy-style docstring subject). - **docs** — builds this site with `--strict`. - **pages** — after CI passes on a push to `main`, deploys this site to [GitHub Pages](https://grAItools.github.io/array-vectorize/). -- **release** — on a pushed `v*` tag, re-runs the gate, builds the sdist - and wheel with `uv build`, and publishes them as a GitHub release - generated from the tag (`.github/workflows/release.yml`). +- **release** — on a pushed `v*` tag, validates the version, lock, changelog, + clean checkout and ancestry on `origin/main`, then runs the full gate. It builds + once with `uv build --no-sources` and tests the wheel and sdist installed into + independent temporary environments. A separate publishing job creates the GitHub + release from those exact artifacts, with generated notes and prerelease marking. + Build permissions are read-only; only publishing can write repository contents. + Runs for the same tag queue rather than cancel during publishing. ## Releases @@ -178,9 +201,18 @@ git tag -a v0.1.0 -m "v0.1.0" git push origin main --tags ``` -`make release` refuses to run when the two version declarations disagree -or the lock is stale; tagging stays a manual step so a re-run of the -target can never move a tag. +`make release` and the workflow share a stdlib validator. The tag must be `v` +plus the exact normalized public Python package version in `pyproject.toml`, +`__version__`, and the project entry in `uv.lock`; local version suffixes and +noncanonical spellings are refused. The changelog must contain that version. +The working tree must be clean, the lock fresh, and HEAD merged into `origin/main` +(fetch origin before releasing). Cleanliness is checked again immediately before +building. Keep `dist/` empty before building: artifact checks require exactly one +wheel and one sdist, preventing upload of leftovers from an earlier release. +Each artifact is installed separately outside the checkout; imports must come +from that environment, match the requested version, carry `py.typed`, and compile +and run a source-backed function correctly. Tagging remains manual; PyPI publishing +and workflow dispatch are not configured. ## Commit messages @@ -205,8 +237,13 @@ to the new format like this: | `ci: x` | `ci: x` (type, no scope) | | `build: x` | `build: x` (type, no scope) | -Existing clones must re-run `make install` once to pick up the new -commit-msg hook. +The hook runner is `prek`; the configuration filename and Git's `pre-commit` +and `commit-msg` stage names stay unchanged. Existing clones must rerun +`make install` after switching runners to replace both installed hooks. +`make hooks` checks all Git-tracked files; staged-only runs and untracked new +files can be silently skipped by filename-filtered hooks. `make lint type` +walks maintained code directly, and the header hook additionally discovers +nonignored untracked Python files. CI uses the same Make checks without autofixing. ## Coding agents @@ -225,7 +262,7 @@ harness's own directory only forwards to them: Edit the shared files; the adapters pick up changes through the import and the symlinks. `tests/test_agent_layout.py` fails when a skill uses harness-specific frontmatter or lacks its `.claude/skills` link. On -Windows, the symlinks need `git config core.symlinks true` (and Developer +Windows, the symlinks need `git config core.symlinks true` before checkout (and Developer Mode or an elevated shell) to check out as links. ## History @@ -234,3 +271,11 @@ The original design document, the restructuring plan, and the log of the 26-round adversarial review loop that drove the exactness work live in git history; the design decisions that still hold are summarized in [Architecture](architecture.md#design-decisions). + +## Portable source files + +Git checkouts use LF through `.gitattributes`. Repository text I/O names UTF-8 +explicitly; generated snippet and snapshot writers name LF explicitly. Tests also +write CRLF and Unicode paths deliberately to verify source inspection on Windows. +No Make installation is required for Windows correctness testing: use +`uv run --frozen pytest -m "not slow" -ra` with `CI=1`. diff --git a/docs/supported-subset.md b/docs/supported-subset.md index 0b43815..218a9bc 100644 --- a/docs/supported-subset.md +++ b/docs/supported-subset.md @@ -11,9 +11,9 @@ Anything outside this subset is **rejected with a precise diagnostic** | `& \| ^ << >> ~` | same operators (integer inputs) | | `abs`, `round`, `min`, `max` | `xp.abs`, `xp.round`, exact left-folded `xp.minimum`/`xp.maximum` | | `int(x)`, `float(x)`, `bool(x)` | `xp.astype(x, xp.int64/float64/bool)` (truncation toward zero) | -| `math.sqrt exp expm1 log log1p log2 log10 sin cos tan asin acos atan atan2 sinh cosh tanh asinh acosh atanh pow floor ceil hypot copysign isnan isinf isfinite` | corresponding `xp.*` | +| `math.sqrt`, `math.exp`, `math.expm1`, `math.log`, `math.log1p`, `math.log2`, `math.log10`, `math.sin`, `math.cos`, `math.tan`, `math.asin`, `math.acos`, `math.atan`, `math.atan2`, `math.sinh`, `math.cosh`, `math.tanh`, `math.asinh`, `math.acosh`, `math.atanh`, `math.pow`, `math.floor`, `math.ceil`, `math.hypot`, `math.copysign`, `math.isnan`, `math.isinf`, `math.isfinite` | corresponding `xp.*` | | `math.trunc(x)` | `xp.astype(x, xp.int64)` | -| `math.pi/e/tau/inf/nan` | literals (`xp.inf` / `xp.nan` for the last two) | +| `math.pi`, `math.e`, `math.tau`, `math.inf`, `math.nan` | literals (`xp.inf` / `xp.nan` for the last two) | | comparisons, chains (`a < b < c`) | elementwise; chains fold with `xp.logical_and` | | `and` / `or` / `not` | exact value-select lowering (`xp.where(x != 0, y, x)`); `xp.logical_*` when both operands are boolean | | ternary `a if c else b` | `xp.where(coerce(c), a, b)` | diff --git a/examples/demo.py b/examples/demo.py index 7cd05a5..ee2e761 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Runnable example: vectorize a few scalar functions and compare backends.""" import math diff --git a/examples/notebooks/01_intro.py b/examples/notebooks/01_intro.py index 9febf1b..3a92ea5 100644 --- a/examples/notebooks/01_intro.py +++ b/examples/notebooks/01_intro.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + import marimo __generated_with = "0.25.1" diff --git a/examples/notebooks/02_backends.py b/examples/notebooks/02_backends.py index acb8167..9eb2745 100644 --- a/examples/notebooks/02_backends.py +++ b/examples/notebooks/02_backends.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + import marimo __generated_with = "0.25.1" diff --git a/examples/notebooks/03_semantics.py b/examples/notebooks/03_semantics.py index e675b0a..8979982 100644 --- a/examples/notebooks/03_semantics.py +++ b/examples/notebooks/03_semantics.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + import marimo __generated_with = "0.25.1" diff --git a/pyproject.toml b/pyproject.toml index 85ebea3..2179ad6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,8 @@ dev = [ "coverage[toml]>=7.6", "numpy>=2.0", "array-api-strict>=2.3", - "pre-commit>=3.5", + "prek>=0.5", + "pyyaml>=6.0.1", # first Python 3.12 wheels; workflow and hook configuration tests "cleanporter>=0.6", ] backends = [ @@ -160,7 +161,7 @@ reason = "golden test data: from-imports deliberately exercise the supported sub [tool.mypy] python_version = "3.12" strict = true -files = ["src"] +files = ["src", "scripts"] [[tool.mypy.overrides]] module = "array_api_compat" @@ -168,12 +169,12 @@ ignore_missing_imports = true # All four checkers are gates; Make owns commands and these sections own scope. [tool.pyright] -include = ["src"] +include = ["src", "scripts"] pythonVersion = "3.12" typeCheckingMode = "standard" [tool.zuban] -files = ["src", "tests"] +files = ["src", "tests", "scripts"] mypy_path = ["src", "tests"] python_version = "3.12" strict = true @@ -207,7 +208,7 @@ module = "array_api_compat" ignore_missing_imports = true [tool.pyrefly] -project-includes = ["src", "tests"] +project-includes = ["src", "tests", "scripts"] project-excludes = [ "tests/golden/cases/**", "tests/corpus.py", diff --git a/scripts/license_headers.py b/scripts/license_headers.py new file mode 100755 index 0000000..3c55e82 --- /dev/null +++ b/scripts/license_headers.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + +"""Check or apply the repository's canonical Python license header. + +Git defines the scope, including untracked, nonignored Python files. This +keeps fixtures and hidden operator scripts covered without touching downloaded +corpora or environments. Work with bytes so fixing a header preserves source +encoding and line endings. Only recognized notices owned by grAItools may be +normalized; conflicting notices require a human decision. +""" + +from __future__ import annotations + +import argparse +import io +import os +import pathlib +import re +import subprocess +import sys +import tokenize + +ROOT = pathlib.Path(__file__).resolve().parents[1] +TEMPLATE = ROOT / ".license-header.txt" +_BOM = b"\xef\xbb\xbf" +_COOKIE = re.compile(rb"^[ \t\f]*#.*?coding[:=][ \t]*[-\w.]+") +_NOTICE = re.compile( + rb"^\s*#\s*(?:copyright\b|spdx-(?:license-identifier|filecopyrighttext)" + rb"|.*\blicensed? under\b|licen[cs]e\s*:|(?:MIT|BSD.*|Apache.*) License\b)", + re.IGNORECASE, +) +_OWNED = re.compile( + rb"^#\s*(?:Copyright\s+\(c\)\s+|SPDX-FileCopyrightText:\s*)" + rb"\d{4}(?:-\d{4})?\s+grAItools\.?$", + re.IGNORECASE, +) +_LICENSE = re.compile(rb"^#\s*SPDX-License-Identifier:\s*BSD-3-Clause$", re.IGNORECASE) +_REFERENCE = re.compile(rb"^# See LICENSE\b.*$", re.IGNORECASE) +_PROJECT = re.compile(rb"^# array-vectorize: .+$") + + +def _files(root: pathlib.Path) -> list[pathlib.Path]: + """Return existing tracked and nonignored untracked Python paths.""" + result = subprocess.run( + ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"], + cwd=root, + capture_output=True, + check=True, + ) + return sorted( + { + root / os.fsdecode(name) + for name in result.stdout.split(b"\0") + if name.endswith((b".py", b".pyi")) + and ((root / os.fsdecode(name)).exists() or (root / os.fsdecode(name)).is_symlink()) + } + ) + + +def _preamble(lines: list[bytes]) -> int: + """Keep shebangs and PEP 263 encoding cookies in their original positions.""" + if len(lines) > 1 and _COOKIE.match(lines[1]): + return 2 + return int(bool(lines and (lines[0].startswith(b"#!") or _COOKIE.match(lines[0])))) + + +def _leading_comments(lines: list[bytes]) -> int: + for index, line in enumerate(lines): + if line.strip() and not line.lstrip().startswith(b"#"): + return index + return len(lines) + + +def _without_header(lines: list[bytes]) -> list[bytes]: + """Remove only recognized owned notices from the leading comment region.""" + end = _leading_comments(lines) + prefix = lines[:end] + notices = [line.strip() for line in prefix if _NOTICE.search(line)] + if notices and not any(_OWNED.fullmatch(line) for line in notices): + msg = "license notice has no recognized grAItools copyright" + raise ValueError(msg) + if any(not (_OWNED.fullmatch(line) or _LICENSE.fullmatch(line)) for line in notices): + msg = "conflicting or unrecognized license notice" + raise ValueError(msg) + if not notices: + return lines + while prefix and not prefix[0].strip(): + prefix.pop(0) + remaining: list[bytes] = [] + after_header = False + in_owned_header = False + for index, line in enumerate(prefix): + if _NOTICE.search(line): + in_owned_header = in_owned_header or bool(_OWNED.fullmatch(line.strip())) + after_header = True + elif (in_owned_header and _REFERENCE.fullmatch(line.strip())) or ( + _PROJECT.fullmatch(line.strip()) + and index + 1 < len(prefix) + and _OWNED.fullmatch(prefix[index + 1].strip()) + ): + after_header = True + else: + in_owned_header = False + if line.strip() or not after_header: + remaining.append(line) + # Consume one header separator; retain subsequent source blank lines. + after_header = False + return remaining + lines[end:] + + +def _normalized(raw: bytes, template: bytes) -> bytes: + """Produce the canonical header while retaining BOM, encoding and newlines.""" + tokenize.detect_encoding(io.BytesIO(raw).readline) + bom = _BOM if raw.startswith(_BOM) else b"" + source = raw[len(bom) :] + lines = source.splitlines(keepends=True) + newline = b"\r\n" if b"\r\n" in source[: source.find(b"\n") + 1] else b"\n" + start = _preamble(lines) + if any(_NOTICE.search(line) for line in lines[:start]): + msg = "license notice before or inside an encoding declaration" + raise ValueError(msg) + _check_placement(raw, start + _leading_comments(lines[start:])) + preamble = b"".join(lines[:start]) + if preamble and not preamble.endswith(b"\n"): + preamble += newline + header = template.replace(b"\n", newline) + newline + return bom + preamble + header + b"".join(_without_header(lines[start:])) + + +def _check_placement(raw: bytes, prefix_end: int) -> None: + """Refuse misplaced notices without mistaking embedded source strings for headers.""" + for token in tokenize.tokenize(io.BytesIO(raw).readline): + if ( + token.type == tokenize.COMMENT + and token.start[0] > prefix_end + and _NOTICE.search(token.string.encode("utf-8")) + ): + msg = "license notice outside the leading header block" + raise ValueError(msg) + + +def _template(path: pathlib.Path) -> bytes: + template = path.read_bytes() + if ( + not template.endswith(b"\n") + or b"\r" in template + or not template.splitlines() + or any(not line.startswith(b"# ") for line in template.splitlines()) + ): + msg = "template must contain LF-terminated comment lines and no blank separator" + raise ValueError(msg) + return template + + +def _process(path: pathlib.Path, template: bytes, *, fix: bool) -> bool: + if path.is_symlink() or any( + (ROOT / parent).is_symlink() for parent in path.relative_to(ROOT).parents + ): + msg = "source symlinks are not supported" + raise ValueError(msg) + raw = path.read_bytes() + expected = _normalized(raw, template) + if raw == expected: + return True + if fix: + path.write_bytes(expected) + return False + + +def main(argv: list[str] | None = None) -> int: + """Check or fix owned headers; return 0, 1, or 2 for success, violations, or errors.""" + parser = argparse.ArgumentParser(description=(__doc__ or "").split("\n")[0]) + modes = parser.add_mutually_exclusive_group(required=True) + modes.add_argument("--check", action="store_true", help="report violations without writing") + modes.add_argument("--fix", action="store_true", help="apply or normalize owned headers") + args = parser.parse_args(argv) + try: + template = _template(TEMPLATE) + files = _files(ROOT) + except (OSError, ValueError, subprocess.CalledProcessError) as exc: + print(f"license headers: {exc}", file=sys.stderr) + return 2 + violations = False + errors = False + for path in files: + try: + valid = _process(path, template, fix=args.fix) + except (OSError, SyntaxError, UnicodeError, tokenize.TokenError) as exc: + print(f"{path.relative_to(ROOT)}: {exc}", file=sys.stderr) + errors = True + except ValueError as exc: + print(f"{path.relative_to(ROOT)}: {exc}", file=sys.stderr) + violations = True + else: + if not valid: + action = "applied header" if args.fix else "missing or inconsistent header" + print(f"{path.relative_to(ROOT)}: {action}") + violations = violations or not args.fix + return 2 if errors else int(violations) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/package_smoke.py b/scripts/package_smoke.py new file mode 100644 index 0000000..e1abcbc --- /dev/null +++ b/scripts/package_smoke.py @@ -0,0 +1,100 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + +"""Install and smoke-test the wheel and sdist in independent temporary environments.""" + +from __future__ import annotations + +import argparse +import os +import pathlib +import subprocess +import sys +import tempfile + +ROOT = pathlib.Path(__file__).resolve().parents[1] +_SMOKE = """import importlib.metadata +import pathlib +import sys +import numpy as np +import array_vectorize + +package = pathlib.Path(array_vectorize.__file__).resolve() +assert pathlib.Path(sys.prefix).resolve() in package.parents, package +assert array_vectorize.__version__ == sys.argv[1] +assert importlib.metadata.version("array-vectorize") == sys.argv[1] +assert package.with_name("py.typed").is_file() + +def scalar(x): + if x < 0: + return -x + return x * 2 + +vec = array_vectorize.vectorize(scalar) +np.testing.assert_array_equal(vec(np.array([-3, 0, 4])), np.array([3, 0, 8])) +""" + + +def artifacts(directory: pathlib.Path) -> list[pathlib.Path]: + """Require exactly one wheel and one sdist, preventing stale release uploads.""" + wheels = sorted(directory.glob("*.whl")) + sdists = sorted(directory.glob("*.tar.gz")) + # uv creates this marker in a fresh output directory; it is not uploaded. + entries = { + path for path in directory.iterdir() if path.name != ".gitignore" or not path.is_file() + } + if len(wheels) != 1 or len(sdists) != 1 or entries != set(wheels + sdists): + raise ValueError( + "dist must contain exactly one wheel and one sdist; remove stale artifacts" + ) + return [wheels[0].resolve(), sdists[0].resolve()] + + +def smoke(artifact: pathlib.Path, version: str) -> None: + """Install an exact artifact and execute source-backed vectorization outside the checkout.""" + environment = os.environ.copy() + # Editable installs and user Python configuration must not shadow the artifact. + for key in ("PYTHONPATH", "PYTHONHOME", "UV_PROJECT_ENVIRONMENT", "VIRTUAL_ENV"): + environment.pop(key, None) + with tempfile.TemporaryDirectory(prefix="array-vectorize-release-") as temporary: + directory = pathlib.Path(temporary) + venv = directory / "venv" + subprocess.run( + ["uv", "venv", "--python", sys.executable, str(venv)], + cwd=directory, + env=environment, + check=True, + ) + python = venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + subprocess.run( + ["uv", "pip", "install", "--python", str(python), str(artifact), "numpy>=2.0"], + cwd=directory, + env=environment, + check=True, + ) + script = directory / "smoke.py" + script.write_text(_SMOKE, encoding="utf-8", newline="\n") + subprocess.run( + [str(python), "-I", str(script), version], cwd=directory, env=environment, check=True + ) + + +def main() -> int: + """Smoke-test both artifacts from dist against the requested release version.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--version", required=True) + parser.add_argument("--dist", type=pathlib.Path, default=ROOT / "dist") + args = parser.parse_args() + try: + for artifact in artifacts(args.dist): + smoke(artifact, args.version) + except (ValueError, OSError, subprocess.CalledProcessError) as error: + print(f"package smoke failed: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/release.py b/scripts/release.py new file mode 100644 index 0000000..07b555d --- /dev/null +++ b/scripts/release.py @@ -0,0 +1,117 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + +"""Validate release metadata and Git state before building distribution artifacts.""" + +from __future__ import annotations + +import argparse +import ast +import pathlib +import re +import subprocess +import sys +import tomllib + +ROOT = pathlib.Path(__file__).resolve().parents[1] +_NUMBER = r"(?:0|[1-9][0-9]*)" +_VERSION = re.compile( + rf"(?:[1-9][0-9]*!)?{_NUMBER}(?:\.{_NUMBER})*" + rf"(?:(?:a|b|rc){_NUMBER})?(?:\.post{_NUMBER})?(?:\.dev{_NUMBER})?" +) + + +def version_from_tag(tag: str) -> str: + """Return a normalized public Python version, rejecting aliases and local versions.""" + if not tag.startswith("v") or _VERSION.fullmatch(tag[1:]) is None: + raise ValueError(f"invalid release tag: {tag!r}; expected v") + return tag[1:] + + +def is_prerelease(version: str) -> bool: + """Identify development and alpha, beta, or release candidate versions.""" + return re.search(r"(?:a|b|rc|\.dev)[0-9]+", version) is not None + + +def validate_metadata(root: pathlib.Path, tag: str) -> str: + """Require matching project, source, lock, and changelog versions.""" + version = version_from_tag(tag) + project = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))["project"] + if project["version"] != version: + raise ValueError("pyproject.toml version does not match release tag") + tree = ast.parse((root / "src/array_vectorize/__init__.py").read_text(encoding="utf-8")) + declarations = [ + node.value.value + for node in tree.body + if isinstance(node, ast.Assign) + and any( + isinstance(target, ast.Name) and target.id == "__version__" for target in node.targets + ) + and isinstance(node.value, ast.Constant) + ] + if declarations != [version]: + raise ValueError("source __version__ does not match release tag") + lock = tomllib.loads((root / "uv.lock").read_text(encoding="utf-8")) + entries = [entry for entry in lock["package"] if entry["name"] == project["name"]] + if len(entries) != 1 or entries[0]["version"] != version: + raise ValueError("uv.lock project version does not match release tag") + changelog = (root / "CHANGELOG.md").read_text(encoding="utf-8") + if re.search(rf"^## \[{re.escape(version)}\](?:\s|$)", changelog, re.MULTILINE) is None: + raise ValueError("CHANGELOG.md has no release section for this version") + return version + + +def check_clean(root: pathlib.Path) -> None: + """Reject changes to tracked files and nonignored untracked files.""" + result = subprocess.run( + ["git", "status", "--porcelain", "--untracked-files=all"], + cwd=root, + capture_output=True, + text=True, + encoding="utf-8", + check=True, + ) + if result.stdout: + raise ValueError("release checkout is dirty; commit or remove changes before releasing") + + +def validate_checkout(root: pathlib.Path) -> None: + """Check cleanliness, ancestry against origin/main, and lock freshness.""" + check_clean(root) + result = subprocess.run( + ["git", "merge-base", "--is-ancestor", "HEAD", "origin/main"], cwd=root, check=False + ) + if result.returncode: + raise ValueError("release commit must be an ancestor of origin/main (fetch origin first)") + subprocess.run(["uv", "lock", "--check"], cwd=root, check=True) + + +def main() -> int: + """Validate a tag or perform a final cleanliness check before building.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--tag", help="release tag, for example v0.1.0") + parser.add_argument("--check-clean", action="store_true", help="check Git cleanliness only") + parser.add_argument( + "--prerelease", action="store_true", help="print tag prerelease status only" + ) + args = parser.parse_args() + if not args.check_clean and not args.tag: + parser.error("--tag is required unless --check-clean is supplied") + try: + if args.check_clean: + check_clean(ROOT) + elif args.prerelease: + print(str(is_prerelease(version_from_tag(args.tag))).lower()) + else: + validate_metadata(ROOT, args.tag) + validate_checkout(ROOT) + except (ValueError, OSError, subprocess.CalledProcessError) as error: + print(f"release validation failed: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/smoke.py b/scripts/smoke.py index f511f3a..83741bf 100644 --- a/scripts/smoke.py +++ b/scripts/smoke.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """End-to-end smoke test (run directly).""" import math @@ -8,7 +13,7 @@ @array_vectorize.vectorize -def psi(x): +def psi(x: float) -> float: """Exponential decay on the positive half, zero below it.""" if x < 0: return 0.0 @@ -16,7 +21,7 @@ def psi(x): @array_vectorize.vectorize -def clamp(x, lo=0.0, hi=1.0): +def clamp(x: float, lo: float = 0.0, hi: float = 1.0) -> float: """Clamp x into [lo, hi].""" if x < lo: return lo @@ -35,8 +40,8 @@ def main() -> None: assert np.allclose(clamp(x), expected_clamp), clamp(x) assert np.allclose(clamp(x, -1.0, 0.5), np.clip(x, -1.0, 0.5)) - print(psi.source) - print(clamp.source) + print(array_vectorize.get_source(psi)) + print(array_vectorize.get_source(clamp)) print("smoke ok") diff --git a/src/array_vectorize/__init__.py b/src/array_vectorize/__init__.py index 16b22ca..de9a2c3 100644 --- a/src/array_vectorize/__init__.py +++ b/src/array_vectorize/__init__.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """vectorize(f): compile a scalar Python function into an Array API vectorized function. Public API re-export only — implementation lives in ``array_vectorize.api`` diff --git a/src/array_vectorize/api.py b/src/array_vectorize/api.py index e1739d8..e0b0ed4 100644 --- a/src/array_vectorize/api.py +++ b/src/array_vectorize/api.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Public entry points: ``vectorize``, ``get_source``, and the helper cache. Option handling and the fallback decision live here; the strict compilation diff --git a/src/array_vectorize/codegen/__init__.py b/src/array_vectorize/codegen/__init__.py index c91337a..c63b816 100644 --- a/src/array_vectorize/codegen/__init__.py +++ b/src/array_vectorize/codegen/__init__.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Codegen: IR -> readable Python source.""" from __future__ import annotations diff --git a/src/array_vectorize/codegen/docstring.py b/src/array_vectorize/codegen/docstring.py index d087397..e24f8ec 100644 --- a/src/array_vectorize/codegen/docstring.py +++ b/src/array_vectorize/codegen/docstring.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Docstring assembly for generated functions. The generated function's docstring carries the original's documentation diff --git a/src/array_vectorize/codegen/emitter.py b/src/array_vectorize/codegen/emitter.py index 7bb29b5..344c002 100644 --- a/src/array_vectorize/codegen/emitter.py +++ b/src/array_vectorize/codegen/emitter.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """IR -> readable Python source. Builds an ``ast.Module`` and uses ``ast.unparse``: correct docstring/literal diff --git a/src/array_vectorize/compat.py b/src/array_vectorize/compat.py index 9ea549a..3ccc37d 100644 --- a/src/array_vectorize/compat.py +++ b/src/array_vectorize/compat.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Shared backend-detection helpers for verify and fallback. ``_is_array``/``_py_scalar`` are the two leaves both the differential diff --git a/src/array_vectorize/emit.py b/src/array_vectorize/emit.py index 3a4966b..9172c7d 100644 --- a/src/array_vectorize/emit.py +++ b/src/array_vectorize/emit.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """GENERATION-TIME loading of vectorized functions (from _runtime). ``compile_vectorized`` runs once, inside ``vectorize()`` on the compiler diff --git a/src/array_vectorize/errors.py b/src/array_vectorize/errors.py index 4403ffb..47170b9 100644 --- a/src/array_vectorize/errors.py +++ b/src/array_vectorize/errors.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Error types and diagnostics for vectorization failures.""" from __future__ import annotations diff --git a/src/array_vectorize/fallback.py b/src/array_vectorize/fallback.py index 7744d9c..cfbc31e 100644 --- a/src/array_vectorize/fallback.py +++ b/src/array_vectorize/fallback.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Opt-in element-loop fallback. When ``vectorize(f, fallback=True)`` (or ``strict=False``) cannot vectorize diff --git a/src/array_vectorize/frontend/__init__.py b/src/array_vectorize/frontend/__init__.py index 2e1f5c1..0620648 100644 --- a/src/array_vectorize/frontend/__init__.py +++ b/src/array_vectorize/frontend/__init__.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Frontend: understand the scalar function (extract, validate, tables). NOTE: the ``validate`` function is deliberately NOT re-exported here — diff --git a/src/array_vectorize/frontend/extract.py b/src/array_vectorize/frontend/extract.py index 2b5dc08..cb74bfb 100644 --- a/src/array_vectorize/frontend/extract.py +++ b/src/array_vectorize/frontend/extract.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Source extraction and closure/constant capture.""" from __future__ import annotations diff --git a/src/array_vectorize/frontend/info.py b/src/array_vectorize/frontend/info.py index 14b14cd..a8c75f5 100644 --- a/src/array_vectorize/frontend/info.py +++ b/src/array_vectorize/frontend/info.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """FunctionInfo, Param — dependency-free types shared by the frontend stages.""" from __future__ import annotations diff --git a/src/array_vectorize/frontend/lambda_id.py b/src/array_vectorize/frontend/lambda_id.py index 8ebcd40..ba543b8 100644 --- a/src/array_vectorize/frontend/lambda_id.py +++ b/src/array_vectorize/frontend/lambda_id.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Lambda identification subsystem: pair a function object with its AST node. Grew out of the review-round fix ``5ebff7d`` (raw ``co_code`` is not stable diff --git a/src/array_vectorize/frontend/tables.py b/src/array_vectorize/frontend/tables.py index ee358f9..96bf045 100644 --- a/src/array_vectorize/frontend/tables.py +++ b/src/array_vectorize/frontend/tables.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Scalar-name -> Array API mapping tables. All ``MATH_FUNCS`` targets must exist on every Array API backend; this is diff --git a/src/array_vectorize/frontend/validate.py b/src/array_vectorize/frontend/validate.py index b0712bd..2403193 100644 --- a/src/array_vectorize/frontend/validate.py +++ b/src/array_vectorize/frontend/validate.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Supported-subset checking with linter-style diagnostics (design D5). Structural checks only (AST shape); semantic checks (name resolution, call diff --git a/src/array_vectorize/fuzz.py b/src/array_vectorize/fuzz.py index 35ba905..cc96ec1 100644 --- a/src/array_vectorize/fuzz.py +++ b/src/array_vectorize/fuzz.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Grammar fuzzer CLI: ``python -m array_vectorize.fuzz``. Generates random programs from the supported grammar, vectorizes them, and @@ -143,7 +148,7 @@ def _run_one(rng: random.Random) -> str: # vectorize() requires inspectable source: define the case in a real file case_dir = pathlib.Path(tempfile.mkdtemp(prefix="array_vectorize_fuzz_")) path = case_dir / "fuzz_case_mod.py" - path.write_text("import math\n\n\n" + src) + path.write_text("import math\n\n\n" + src, encoding="utf-8", newline="\n") spec = importlib.util.spec_from_file_location("fuzz_case_mod", path) assert spec is not None assert spec.loader is not None diff --git a/src/array_vectorize/ir/__init__.py b/src/array_vectorize/ir/__init__.py index f370a25..3b5c43a 100644 --- a/src/array_vectorize/ir/__init__.py +++ b/src/array_vectorize/ir/__init__.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """The array_vectorize IR: node dataclasses, SSA naming, and the tree algebra.""" from __future__ import annotations diff --git a/src/array_vectorize/ir/nodes.py b/src/array_vectorize/ir/nodes.py index 426e2ca..2931d86 100644 --- a/src/array_vectorize/ir/nodes.py +++ b/src/array_vectorize/ir/nodes.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """IR node dataclasses and the is_bool lattice. IR: frozen, hashable dataclasses. Hashability enables structural diff --git a/src/array_vectorize/ir/ssa.py b/src/array_vectorize/ir/ssa.py index 366c684..1cc6f1d 100644 --- a/src/array_vectorize/ir/ssa.py +++ b/src/array_vectorize/ir/ssa.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """SSA name allocation (from _ir): generated names and the SSA environment.""" from __future__ import annotations diff --git a/src/array_vectorize/ir/walk.py b/src/array_vectorize/ir/walk.py index 366c6ff..293d519 100644 --- a/src/array_vectorize/ir/walk.py +++ b/src/array_vectorize/ir/walk.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Generic IR tree algebra: child enumeration and bottom-up rebuilding. ``children``/``rewrite`` moved from ``_optimize`` (restructure phase 2): the diff --git a/src/array_vectorize/lower/__init__.py b/src/array_vectorize/lower/__init__.py index 97e1e32..9352618 100644 --- a/src/array_vectorize/lower/__init__.py +++ b/src/array_vectorize/lower/__init__.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """AST -> IR lowering. The package assembles ``_Lowerer`` from one concern per layer — state diff --git a/src/array_vectorize/lower/env.py b/src/array_vectorize/lower/env.py index 9017069..444855b 100644 --- a/src/array_vectorize/lower/env.py +++ b/src/array_vectorize/lower/env.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Lowering state and the shared environment machinery. ``_LowererBase`` owns every piece of mutable lowering state — SSA name diff --git a/src/array_vectorize/lower/expressions.py b/src/array_vectorize/lower/expressions.py index 3d7549c..2c2e4e9 100644 --- a/src/array_vectorize/lower/expressions.py +++ b/src/array_vectorize/lower/expressions.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Expression lowering: AST expressions to IR nodes. Covers binops (with the exactness fixes that route boolean-adjacent diff --git a/src/array_vectorize/lower/helpers.py b/src/array_vectorize/lower/helpers.py index b2de4d8..e4b051d 100644 --- a/src/array_vectorize/lower/helpers.py +++ b/src/array_vectorize/lower/helpers.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """User-helper lowering: vectorizing and calling other scalar functions. Calls to functions in the enclosing scope (``info.user_funcs``) are diff --git a/src/array_vectorize/lower/kinds.py b/src/array_vectorize/lower/kinds.py index 518ee70..87ce497 100644 --- a/src/array_vectorize/lower/kinds.py +++ b/src/array_vectorize/lower/kinds.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """The explicit kind lattice: per-name kind-inference facts. One :class:`VarInfo` per tracked (emitted) name consolidates the diff --git a/src/array_vectorize/lower/loops.py b/src/array_vectorize/lower/loops.py index 7a2ba4f..849bbd5 100644 --- a/src/array_vectorize/lower/loops.py +++ b/src/array_vectorize/lower/loops.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Loop lowering: constant-trip ``for`` loops with loop-carried phis. A loop's carried variables (bound before the loop, reassigned in the diff --git a/src/array_vectorize/lower/sanitize.py b/src/array_vectorize/lower/sanitize.py index c1d22ea..53ab853 100644 --- a/src/array_vectorize/lower/sanitize.py +++ b/src/array_vectorize/lower/sanitize.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Argument sanitization for Array API call sites. Operators promote raw Python scalars fine, but ``xp.*`` function diff --git a/src/array_vectorize/lower/statements.py b/src/array_vectorize/lower/statements.py index b186db1..c87d831 100644 --- a/src/array_vectorize/lower/statements.py +++ b/src/array_vectorize/lower/statements.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Statement lowering: assignments and if/elif/else branches. Assignments allocate SSA names and record kind/literal/scalar facts; diff --git a/src/array_vectorize/lower/types.py b/src/array_vectorize/lower/types.py index 4edd7d8..e286282 100644 --- a/src/array_vectorize/lower/types.py +++ b/src/array_vectorize/lower/types.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Lowering output types shared across the pipeline. ``LoweredFunction`` is the contract between lowering and everything diff --git a/src/array_vectorize/optimize/__init__.py b/src/array_vectorize/optimize/__init__.py index d126b94..a97e49c 100644 --- a/src/array_vectorize/optimize/__init__.py +++ b/src/array_vectorize/optimize/__init__.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Optimizer passes: const-fold, CSE, DCE and the pass pipeline. NOTE on imports: ``from array_vectorize.optimize import cse`` (from-import) is diff --git a/src/array_vectorize/optimize/constfold.py b/src/array_vectorize/optimize/constfold.py index fda3d87..562cbcb 100644 --- a/src/array_vectorize/optimize/constfold.py +++ b/src/array_vectorize/optimize/constfold.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Constant folding over the IR.""" from __future__ import annotations diff --git a/src/array_vectorize/optimize/cse.py b/src/array_vectorize/optimize/cse.py index 4cea993..06c6c6c 100644 --- a/src/array_vectorize/optimize/cse.py +++ b/src/array_vectorize/optimize/cse.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Common-subexpression elimination over the IR.""" from __future__ import annotations diff --git a/src/array_vectorize/optimize/dce.py b/src/array_vectorize/optimize/dce.py index 5fabec6..8243ad6 100644 --- a/src/array_vectorize/optimize/dce.py +++ b/src/array_vectorize/optimize/dce.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Dead-code elimination over the IR.""" from __future__ import annotations diff --git a/src/array_vectorize/optimize/protect_domains.py b/src/array_vectorize/optimize/protect_domains.py index db10220..fad1878 100644 --- a/src/array_vectorize/optimize/protect_domains.py +++ b/src/array_vectorize/optimize/protect_domains.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """protect_domains: clamp partial-function arguments on dead lanes (design D1).""" from __future__ import annotations diff --git a/src/array_vectorize/pipeline.py b/src/array_vectorize/pipeline.py index ddfe1c1..7768fc1 100644 --- a/src/array_vectorize/pipeline.py +++ b/src/array_vectorize/pipeline.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """The vectorization pipeline: extract -> validate -> lower -> optimize -> codegen -> compile. This module owns the strict (non-fallback) compilation stages only; option diff --git a/src/array_vectorize/runtime/__init__.py b/src/array_vectorize/runtime/__init__.py index 16bd655..3e0bd31 100644 --- a/src/array_vectorize/runtime/__init__.py +++ b/src/array_vectorize/runtime/__init__.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """CALL-TIME runtime library injected into generated modules (from _runtime). Executes on the user's backend when a vectorized function is called. diff --git a/src/array_vectorize/runtime/arith.py b/src/array_vectorize/runtime/arith.py index 9e52f4a..410cab4 100644 --- a/src/array_vectorize/runtime/arith.py +++ b/src/array_vectorize/runtime/arith.py @@ -1,3 +1,4 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. # Copyright (c) 2026 grAItools # SPDX-License-Identifier: BSD-3-Clause # See LICENSE for the full license text. diff --git a/src/array_vectorize/runtime/dtype.py b/src/array_vectorize/runtime/dtype.py index fbe4a51..9f7623c 100644 --- a/src/array_vectorize/runtime/dtype.py +++ b/src/array_vectorize/runtime/dtype.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Dtype analysis for the call-time helpers (from _runtime). Classifies values/dtypes for promotion decisions: width extraction, diff --git a/src/array_vectorize/runtime/minmax.py b/src/array_vectorize/runtime/minmax.py index 246aa66..88909d8 100644 --- a/src/array_vectorize/runtime/minmax.py +++ b/src/array_vectorize/runtime/minmax.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Exact-semantics min/max helper (from _runtime). ``_vec_minmax`` is injected into generated modules and executes on the diff --git a/src/array_vectorize/runtime/registry.py b/src/array_vectorize/runtime/registry.py index f639a24..9abbdfe 100644 --- a/src/array_vectorize/runtime/registry.py +++ b/src/array_vectorize/runtime/registry.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """The runtime-helper registry: the seam between lowering and emitted calls. Lowering decides WHERE a runtime helper is needed; this registry is the diff --git a/src/array_vectorize/verify.py b/src/array_vectorize/verify.py index a4cee27..32583b8 100644 --- a/src/array_vectorize/verify.py +++ b/src/array_vectorize/verify.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Differential-verification helper. ``vectorize(f, verify=example_args)`` runs the generated function and the diff --git a/tests/backends/test_array_api_strict.py b/tests/backends/test_array_api_strict.py index f81634a..6f55836 100644 --- a/tests/backends/test_array_api_strict.py +++ b/tests/backends/test_array_api_strict.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """array-api-strict backend twins of the behavior tests.""" from __future__ import annotations diff --git a/tests/backends/test_jax_torch_compilation.py b/tests/backends/test_jax_torch_compilation.py index 84fba93..a4833bd 100644 --- a/tests/backends/test_jax_torch_compilation.py +++ b/tests/backends/test_jax_torch_compilation.py @@ -1,3 +1,4 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. # Copyright (c) 2026 grAItools # SPDX-License-Identifier: BSD-3-Clause # See LICENSE for the full license text. diff --git a/tests/behavior/test_arithmetic_dtypes.py b/tests/behavior/test_arithmetic_dtypes.py index 35d36f8..030220c 100644 --- a/tests/behavior/test_arithmetic_dtypes.py +++ b/tests/behavior/test_arithmetic_dtypes.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Bool/int/uint64 arithmetic exactness: dtype promotion, literals, min/max, bitwise, negation.""" from __future__ import annotations diff --git a/tests/behavior/test_calls_math_casts.py b/tests/behavior/test_calls_math_casts.py index 7211f44..6d95c71 100644 --- a/tests/behavior/test_calls_math_casts.py +++ b/tests/behavior/test_calls_math_casts.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Math and builtin calls, casts, pow, floor-mod, abs/round, scalar-argument math.""" from __future__ import annotations diff --git a/tests/behavior/test_closures.py b/tests/behavior/test_closures.py index dcb1abe..a9dbd94 100644 --- a/tests/behavior/test_closures.py +++ b/tests/behavior/test_closures.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Closures over scalars and arrays, keyword-only defaults.""" from __future__ import annotations diff --git a/tests/behavior/test_control_flow.py b/tests/behavior/test_control_flow.py index 0e89b52..f0e5d05 100644 --- a/tests/behavior/test_control_flow.py +++ b/tests/behavior/test_control_flow.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Branch merging, early returns, ternaries, piecewise functions, SSA rebinds.""" from __future__ import annotations diff --git a/tests/behavior/test_cse.py b/tests/behavior/test_cse.py index 5b23e3e..c7e710d 100644 --- a/tests/behavior/test_cse.py +++ b/tests/behavior/test_cse.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """CSE temps: opportunities, SSA binding collisions, signed zeros.""" from __future__ import annotations diff --git a/tests/behavior/test_divergences.py b/tests/behavior/test_divergences.py index dc3877a..f125e29 100644 --- a/tests/behavior/test_divergences.py +++ b/tests/behavior/test_divergences.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Documented divergences between scalar Python semantics and vectorized backends.""" from __future__ import annotations diff --git a/tests/behavior/test_docstrings.py b/tests/behavior/test_docstrings.py index 12fcde9..3def5f6 100644 --- a/tests/behavior/test_docstrings.py +++ b/tests/behavior/test_docstrings.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Docstrings on vectorized functions: summary prefix, source round trip, Notes merging.""" from __future__ import annotations diff --git a/tests/behavior/test_helpers.py b/tests/behavior/test_helpers.py index 8e3f295..e530ea4 100644 --- a/tests/behavior/test_helpers.py +++ b/tests/behavior/test_helpers.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Helper vectorization and caching, name shadowing, rejections.""" from __future__ import annotations diff --git a/tests/behavior/test_inspectability.py b/tests/behavior/test_inspectability.py index 7bc98a7..71ac2cd 100644 --- a/tests/behavior/test_inspectability.py +++ b/tests/behavior/test_inspectability.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Inspectability of vectorized functions: getsource, linecache, tracebacks, help().""" from __future__ import annotations diff --git a/tests/behavior/test_lambdas.py b/tests/behavior/test_lambdas.py index bace100..c8fb775 100644 --- a/tests/behavior/test_lambdas.py +++ b/tests/behavior/test_lambdas.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Vectorizing lambdas: identification, naming, values.""" from __future__ import annotations diff --git a/tests/behavior/test_loops.py b/tests/behavior/test_loops.py index 261cb4c..0e3de9d 100644 --- a/tests/behavior/test_loops.py +++ b/tests/behavior/test_loops.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """For-loops: phis, zero-trip semantics, kind mixing, nested and chained loops.""" from __future__ import annotations diff --git a/tests/behavior/test_memoization.py b/tests/behavior/test_memoization.py index bad0520..0d71159 100644 --- a/tests/behavior/test_memoization.py +++ b/tests/behavior/test_memoization.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Memoization of vectorize(): canonical cache, backrefs, idempotent revectorization.""" from __future__ import annotations diff --git a/tests/behavior/test_name_allocation.py b/tests/behavior/test_name_allocation.py index e07db9f..9aabbd7 100644 --- a/tests/behavior/test_name_allocation.py +++ b/tests/behavior/test_name_allocation.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Generated-name allocation: reserved params, shadowed helper names.""" from __future__ import annotations diff --git a/tests/behavior/test_public_api.py b/tests/behavior/test_public_api.py index b410cd6..84e405e 100644 --- a/tests/behavior/test_public_api.py +++ b/tests/behavior/test_public_api.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Public API surface of the array_vectorize package.""" from __future__ import annotations diff --git a/tests/conftest.py b/tests/conftest.py index b5874ad..50fd63b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Shared pytest options.""" from __future__ import annotations diff --git a/tests/corpus.py b/tests/corpus.py index 26dc042..dfa956e 100644 --- a/tests/corpus.py +++ b/tests/corpus.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Corpus of scalar functions for golden-source tests. Every function here must be defined at module level in this real file so diff --git a/tests/differential/test_scalar_equivalence.py b/tests/differential/test_scalar_equivalence.py index 011fac1..85bc572 100644 --- a/tests/differential/test_scalar_equivalence.py +++ b/tests/differential/test_scalar_equivalence.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Differential tests vs the scalar original using Hypothesis.""" from __future__ import annotations diff --git a/tests/features/test_fallback.py b/tests/features/test_fallback.py index 855b19e..6c46008 100644 --- a/tests/features/test_fallback.py +++ b/tests/features/test_fallback.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """fallback=True / strict=False element-loop fallback behavior.""" from __future__ import annotations diff --git a/tests/features/test_namespace_pin.py b/tests/features/test_namespace_pin.py index 44bfb1f..80d84e9 100644 --- a/tests/features/test_namespace_pin.py +++ b/tests/features/test_namespace_pin.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """namespace= pinning and with_namespace variants (Strategy A). A pinned vectorized function binds ``xp`` directly to the provided @@ -40,9 +45,13 @@ def test_pinned_source_shape() -> None: def test_unpinned_source_unchanged() -> None: unpinned = array_vectorize.vectorize(corpus.add) pinned = array_vectorize.vectorize(corpus.add, namespace=np) - # byte-identical to the pre-pin generated source (the golden snapshot) - golden = (pathlib.Path(__file__).parent.parent / "golden" / "cases" / "add.py").read_text() - assert support.with_metadata(unpinned).source == golden + # Byte-identical to the golden source, excluding its repository header. + golden = (pathlib.Path(__file__).parent.parent / "golden" / "cases" / "add.py").read_text( + encoding="utf-8" + ) + header = (pathlib.Path(__file__).parents[2] / ".license-header.txt").read_text(encoding="utf-8") + assert golden.startswith(header + "\n") + assert support.with_metadata(unpinned).source == golden.removeprefix(header + "\n") assert support.with_metadata(pinned).source != support.with_metadata(unpinned).source assert "xp = _namespace" not in support.with_metadata(unpinned).source assert "_namespace=None" not in support.with_metadata(unpinned).source diff --git a/tests/features/test_protect_domains.py b/tests/features/test_protect_domains.py index 1cd9162..77c21f5 100644 --- a/tests/features/test_protect_domains.py +++ b/tests/features/test_protect_domains.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """protect_domains=True dead-lane clamping semantics.""" from __future__ import annotations diff --git a/tests/features/test_verify.py b/tests/features/test_verify.py index 9a18ea3..800d9f8 100644 --- a/tests/features/test_verify.py +++ b/tests/features/test_verify.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """verify= differential verification option.""" from __future__ import annotations diff --git a/tests/golden/cases/abs_round.py b/tests/golden/cases/abs_round.py index d78f297..623d0f5 100644 --- a/tests/golden/cases/abs_round.py +++ b/tests/golden/cases/abs_round.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def abs_round_vec(x): diff --git a/tests/golden/cases/add.py b/tests/golden/cases/add.py index 60681d7..403f7b7 100644 --- a/tests/golden/cases/add.py +++ b/tests/golden/cases/add.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def add_vec(x, y): diff --git a/tests/golden/cases/add_pinned.py b/tests/golden/cases/add_pinned.py index a626bbf..0bf9ba0 100644 --- a/tests/golden/cases/add_pinned.py +++ b/tests/golden/cases/add_pinned.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + def add_vec(x, y, *, _namespace=None): """ (array-vectorized) no docstring on the scalar original. diff --git a/tests/golden/cases/arith_ops.py b/tests/golden/cases/arith_ops.py index b43a6d1..1dcd877 100644 --- a/tests/golden/cases/arith_ops.py +++ b/tests/golden/cases/arith_ops.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def arith_ops_vec(x, y): diff --git a/tests/golden/cases/bitwise_ops.py b/tests/golden/cases/bitwise_ops.py index 2f56f5a..114b3dd 100644 --- a/tests/golden/cases/bitwise_ops.py +++ b/tests/golden/cases/bitwise_ops.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def bitwise_ops_vec(x, y): diff --git a/tests/golden/cases/boolop_logical.py b/tests/golden/cases/boolop_logical.py index d9d2b59..e48bbce 100644 --- a/tests/golden/cases/boolop_logical.py +++ b/tests/golden/cases/boolop_logical.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def boolop_logical_vec(x, y): diff --git a/tests/golden/cases/boolop_numeric.py b/tests/golden/cases/boolop_numeric.py index 646667c..b5eaf8b 100644 --- a/tests/golden/cases/boolop_numeric.py +++ b/tests/golden/cases/boolop_numeric.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def boolop_numeric_vec(x, y): diff --git a/tests/golden/cases/both_branches_assign.py b/tests/golden/cases/both_branches_assign.py index ef4291f..177cbe9 100644 --- a/tests/golden/cases/both_branches_assign.py +++ b/tests/golden/cases/both_branches_assign.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def both_branches_assign_vec(x): diff --git a/tests/golden/cases/casts.py b/tests/golden/cases/casts.py index e3c01ef..7fd34bd 100644 --- a/tests/golden/cases/casts.py +++ b/tests/golden/cases/casts.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def casts_vec(x): diff --git a/tests/golden/cases/clamp.py b/tests/golden/cases/clamp.py index ef739b8..d57efd7 100644 --- a/tests/golden/cases/clamp.py +++ b/tests/golden/cases/clamp.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def clamp_vec(x, lo=0.0, hi=1.0): diff --git a/tests/golden/cases/closure_array.py b/tests/golden/cases/closure_array.py index b967970..788d09d 100644 --- a/tests/golden/cases/closure_array.py +++ b/tests/golden/cases/closure_array.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def closure_array_vec(x, *, ARR=None): diff --git a/tests/golden/cases/closure_scalar.py b/tests/golden/cases/closure_scalar.py index 0256ace..811a222 100644 --- a/tests/golden/cases/closure_scalar.py +++ b/tests/golden/cases/closure_scalar.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def closure_scalar_vec(x): diff --git a/tests/golden/cases/compare_chain.py b/tests/golden/cases/compare_chain.py index 26163f4..8674c2b 100644 --- a/tests/golden/cases/compare_chain.py +++ b/tests/golden/cases/compare_chain.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def compare_chain_vec(x): diff --git a/tests/golden/cases/compare_ops.py b/tests/golden/cases/compare_ops.py index 9619899..29cf224 100644 --- a/tests/golden/cases/compare_ops.py +++ b/tests/golden/cases/compare_ops.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def compare_ops_vec(x, y): diff --git a/tests/golden/cases/complex_expr.py b/tests/golden/cases/complex_expr.py index e5771b1..c3b10c4 100644 --- a/tests/golden/cases/complex_expr.py +++ b/tests/golden/cases/complex_expr.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def complex_expr_vec(x, y): diff --git a/tests/golden/cases/constants.py b/tests/golden/cases/constants.py index 93ffa0e..81f492f 100644 --- a/tests/golden/cases/constants.py +++ b/tests/golden/cases/constants.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def constants_vec(x): diff --git a/tests/golden/cases/cse_opportunity.py b/tests/golden/cases/cse_opportunity.py index d51b75e..f78b058 100644 --- a/tests/golden/cases/cse_opportunity.py +++ b/tests/golden/cases/cse_opportunity.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def cse_opportunity_vec(x): diff --git a/tests/golden/cases/docstring_existing_notes.py b/tests/golden/cases/docstring_existing_notes.py index 9e6ebf6..e42c892 100644 --- a/tests/golden/cases/docstring_existing_notes.py +++ b/tests/golden/cases/docstring_existing_notes.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def docstring_existing_notes_vec(x): diff --git a/tests/golden/cases/docstring_google.py b/tests/golden/cases/docstring_google.py index 66181ef..5f1e4d9 100644 --- a/tests/golden/cases/docstring_google.py +++ b/tests/golden/cases/docstring_google.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def docstring_google_vec(x, y): diff --git a/tests/golden/cases/docstring_numpy.py b/tests/golden/cases/docstring_numpy.py index d91641f..4dd8a47 100644 --- a/tests/golden/cases/docstring_numpy.py +++ b/tests/golden/cases/docstring_numpy.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def docstring_numpy_vec(x, y): diff --git a/tests/golden/cases/docstring_numpy_notes.py b/tests/golden/cases/docstring_numpy_notes.py index 1ba2831..29bfec1 100644 --- a/tests/golden/cases/docstring_numpy_notes.py +++ b/tests/golden/cases/docstring_numpy_notes.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def docstring_numpy_notes_vec(x): diff --git a/tests/golden/cases/floor_mod_ops.py b/tests/golden/cases/floor_mod_ops.py index 2640101..560402d 100644 --- a/tests/golden/cases/floor_mod_ops.py +++ b/tests/golden/cases/floor_mod_ops.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def floor_mod_ops_vec(x): diff --git a/tests/golden/cases/from_import.py b/tests/golden/cases/from_import.py index 8ad6de8..dce42e4 100644 --- a/tests/golden/cases/from_import.py +++ b/tests/golden/cases/from_import.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def from_import_vec(x): diff --git a/tests/golden/cases/helper_outer.py b/tests/golden/cases/helper_outer.py index 490bc32..b6bdc54 100644 --- a/tests/golden/cases/helper_outer.py +++ b/tests/golden/cases/helper_outer.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def helper_outer_vec(x): diff --git a/tests/golden/cases/kwonly_defaults.py b/tests/golden/cases/kwonly_defaults.py index eb57fe3..9aa6788 100644 --- a/tests/golden/cases/kwonly_defaults.py +++ b/tests/golden/cases/kwonly_defaults.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def kwonly_defaults_vec(x, *, scale=2.0, bias=0.5): diff --git a/tests/golden/cases/loop_accumulate.py b/tests/golden/cases/loop_accumulate.py index e6066e8..6bd7573 100644 --- a/tests/golden/cases/loop_accumulate.py +++ b/tests/golden/cases/loop_accumulate.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def loop_accumulate_vec(x): diff --git a/tests/golden/cases/loop_helper_caller.py b/tests/golden/cases/loop_helper_caller.py index b0a16f9..a72fb3f 100644 --- a/tests/golden/cases/loop_helper_caller.py +++ b/tests/golden/cases/loop_helper_caller.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def loop_helper_caller_vec(x): diff --git a/tests/golden/cases/loop_start_stop_step.py b/tests/golden/cases/loop_start_stop_step.py index 6ba0ed8..ed9c0a2 100644 --- a/tests/golden/cases/loop_start_stop_step.py +++ b/tests/golden/cases/loop_start_stop_step.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def loop_start_stop_step_vec(x): diff --git a/tests/golden/cases/loop_two_carried.py b/tests/golden/cases/loop_two_carried.py index 95ec010..dd9745f 100644 --- a/tests/golden/cases/loop_two_carried.py +++ b/tests/golden/cases/loop_two_carried.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def loop_two_carried_vec(x): diff --git a/tests/golden/cases/loop_with_branch.py b/tests/golden/cases/loop_with_branch.py index c100bf8..3b2eaca 100644 --- a/tests/golden/cases/loop_with_branch.py +++ b/tests/golden/cases/loop_with_branch.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def loop_with_branch_vec(x): diff --git a/tests/golden/cases/math_calls.py b/tests/golden/cases/math_calls.py index eca97e7..b7db9f3 100644 --- a/tests/golden/cases/math_calls.py +++ b/tests/golden/cases/math_calls.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def math_calls_vec(x): diff --git a/tests/golden/cases/math_floor_ceil.py b/tests/golden/cases/math_floor_ceil.py index 4771b76..40c9b10 100644 --- a/tests/golden/cases/math_floor_ceil.py +++ b/tests/golden/cases/math_floor_ceil.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def math_floor_ceil_vec(x): diff --git a/tests/golden/cases/math_two_arg.py b/tests/golden/cases/math_two_arg.py index d989047..b83c6ed 100644 --- a/tests/golden/cases/math_two_arg.py +++ b/tests/golden/cases/math_two_arg.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def math_two_arg_vec(x, y): diff --git a/tests/golden/cases/merge_with_prior.py b/tests/golden/cases/merge_with_prior.py index a2a2602..3a6de3f 100644 --- a/tests/golden/cases/merge_with_prior.py +++ b/tests/golden/cases/merge_with_prior.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def merge_with_prior_vec(x): diff --git a/tests/golden/cases/minmax.py b/tests/golden/cases/minmax.py index d334a23..c8de5f2 100644 --- a/tests/golden/cases/minmax.py +++ b/tests/golden/cases/minmax.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def minmax_vec(x, y): diff --git a/tests/golden/cases/nested_early_returns.py b/tests/golden/cases/nested_early_returns.py index 220dc62..bfc7395 100644 --- a/tests/golden/cases/nested_early_returns.py +++ b/tests/golden/cases/nested_early_returns.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def nested_early_returns_vec(x): diff --git a/tests/golden/cases/nested_loops.py b/tests/golden/cases/nested_loops.py index 984882e..f6d5875 100644 --- a/tests/golden/cases/nested_loops.py +++ b/tests/golden/cases/nested_loops.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def nested_loops_vec(x): diff --git a/tests/golden/cases/piecewise.py b/tests/golden/cases/piecewise.py index 5af4013..d03d3ef 100644 --- a/tests/golden/cases/piecewise.py +++ b/tests/golden/cases/piecewise.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def piecewise_vec(x): diff --git a/tests/golden/cases/pow_op.py b/tests/golden/cases/pow_op.py index 00f8e8e..34bb1ce 100644 --- a/tests/golden/cases/pow_op.py +++ b/tests/golden/cases/pow_op.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def pow_op_vec(x): diff --git a/tests/golden/cases/psi.py b/tests/golden/cases/psi.py index 0b3c51d..f40ea02 100644 --- a/tests/golden/cases/psi.py +++ b/tests/golden/cases/psi.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def psi_vec(x): diff --git a/tests/golden/cases/relu.py b/tests/golden/cases/relu.py index 2b86336..b161af3 100644 --- a/tests/golden/cases/relu.py +++ b/tests/golden/cases/relu.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def relu_vec(x): diff --git a/tests/golden/cases/relu_with_else.py b/tests/golden/cases/relu_with_else.py index 6d98f7e..814bac1 100644 --- a/tests/golden/cases/relu_with_else.py +++ b/tests/golden/cases/relu_with_else.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def relu_with_else_vec(x): diff --git a/tests/golden/cases/reserved_param.py b/tests/golden/cases/reserved_param.py index 1e39663..38b099a 100644 --- a/tests/golden/cases/reserved_param.py +++ b/tests/golden/cases/reserved_param.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def reserved_param_vec(xp): diff --git a/tests/golden/cases/ssa_rebind.py b/tests/golden/cases/ssa_rebind.py index 63a01e8..0822fc8 100644 --- a/tests/golden/cases/ssa_rebind.py +++ b/tests/golden/cases/ssa_rebind.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def ssa_rebind_vec(x): diff --git a/tests/golden/cases/ternary.py b/tests/golden/cases/ternary.py index 24cc3fb..74f9f71 100644 --- a/tests/golden/cases/ternary.py +++ b/tests/golden/cases/ternary.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def ternary_vec(x): diff --git a/tests/golden/cases/unary_ops.py b/tests/golden/cases/unary_ops.py index 3776f7f..3789284 100644 --- a/tests/golden/cases/unary_ops.py +++ b/tests/golden/cases/unary_ops.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + from array_api_compat import array_namespace def unary_ops_vec(x): diff --git a/tests/golden/test_golden.py b/tests/golden/test_golden.py index 1848f5e..eb359dc 100644 --- a/tests/golden/test_golden.py +++ b/tests/golden/test_golden.py @@ -1,9 +1,15 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Golden source snapshots: ast.dump equality against tests/golden/cases.""" from __future__ import annotations import ast import pathlib +import sys import corpus import numpy as np @@ -13,6 +19,7 @@ import array_vectorize GOLDEN_DIR = pathlib.Path(__file__).parent / "cases" +HEADER = (pathlib.Path(__file__).parents[2] / ".license-header.txt").read_text(encoding="utf-8") #: pinned-variant goldens (vectorize(namespace=np)): the namespace object #: never appears in the source text, only the ``xp = _namespace`` binding @@ -47,10 +54,10 @@ def test_golden_source(name: str, request: pytest.FixtureRequest) -> None: path = _golden_path(name) if request.config.getoption("--update-golden"): GOLDEN_DIR.mkdir(parents=True, exist_ok=True) - path.write_text(source) + path.write_text(HEADER + "\n" + source, encoding="utf-8", newline="\n") return assert path.exists(), f"missing golden file {path}; run pytest --update-golden" - golden = path.read_text() + golden = path.read_text(encoding="utf-8") assert _tree(source) == _tree(golden), ( f"generated source differs from golden for {name!r}; " "review the diff and regenerate with pytest --update-golden if intended\n" @@ -64,10 +71,10 @@ def test_golden_pinned_source(name: str, request: pytest.FixtureRequest) -> None path = _pinned_golden_path(name) if request.config.getoption("--update-golden"): GOLDEN_DIR.mkdir(parents=True, exist_ok=True) - path.write_text(source) + path.write_text(HEADER + "\n" + source, encoding="utf-8", newline="\n") return assert path.exists(), f"missing golden file {path}; run pytest --update-golden" - golden = path.read_text() + golden = path.read_text(encoding="utf-8") assert _tree(source) == _tree(golden), ( f"generated pinned source differs from golden for {name!r}; " "review the diff and regenerate with pytest --update-golden if intended\n" @@ -77,4 +84,19 @@ def test_golden_pinned_source(name: str, request: pytest.FixtureRequest) -> None def test_golden_files_are_valid_python() -> None: for path in GOLDEN_DIR.glob("*.py"): - ast.parse(path.read_text()) + ast.parse(path.read_text(encoding="utf-8")) + + +def test_regenerated_snapshots_keep_headers_without_changing_function_source( + tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest +) -> None: + monkeypatch.setattr(sys.modules[__name__], "GOLDEN_DIR", tmp_path) + monkeypatch.setattr(request.config.option, "update_golden", True) + test_golden_source("add", request) + test_golden_pinned_source("add", request) + for path, source in ( + (_golden_path("add"), _generated_source("add")), + (_pinned_golden_path("add"), _generated_pinned_source("add")), + ): + assert path.read_text(encoding="utf-8") == HEADER + "\n" + source + assert not source.startswith(HEADER) diff --git a/tests/perf/test_bench.py b/tests/perf/test_bench.py index c2ba827..a424c6e 100644 --- a/tests/perf/test_bench.py +++ b/tests/perf/test_bench.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Benchmark suite (pytest-benchmark, slow-marked). Measures three things: diff --git a/tests/perf/test_speed_gate.py b/tests/perf/test_speed_gate.py index c4da1ae..596e92e 100644 --- a/tests/perf/test_speed_gate.py +++ b/tests/perf/test_speed_gate.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Performance smoke test (slow-marked). Two gates: diff --git a/tests/support.py b/tests/support.py index 413e278..375dcd6 100644 --- a/tests/support.py +++ b/tests/support.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Shared test helpers (importable as ``support`` via pytest's pythonpath). Black-box tests build their subjects from source snippets so each case is a @@ -73,7 +78,9 @@ def make_fn(body: str, extra: str = "", defaults: str = "x, y=2.0") -> Callable[ + defaults + "):\n" + body - + "\n" + + "\n", + encoding="utf-8", + newline="\n", ) spec = importlib.util.spec_from_file_location(path.stem, path) assert spec is not None @@ -86,7 +93,7 @@ def make_fn(body: str, extra: str = "", defaults: str = "x, y=2.0") -> Callable[ def make_module(source: str) -> Any: """Write a standalone module from ``source`` and return the module.""" path = TMPDIR / f"module_{next(_seq)}.py" - path.write_text(source) + path.write_text(source, encoding="utf-8", newline="\n") spec = importlib.util.spec_from_file_location(path.stem, path) assert spec is not None assert spec.loader is not None diff --git a/tests/test_agent_layout.py b/tests/test_agent_layout.py index 6d5ea98..2a65a05 100644 --- a/tests/test_agent_layout.py +++ b/tests/test_agent_layout.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Coding-agent configuration layout (see "Agent configuration" in AGENTS.md). The harness-agnostic sources are AGENTS.md and .agents/; harness @@ -24,7 +29,7 @@ def _frontmatter(path: pathlib.Path) -> dict[str, str | list[str]]: """Top-level keys of a markdown file's YAML frontmatter (scalars, lists).""" - lines = path.read_text().splitlines() + lines = path.read_text(encoding="utf-8").splitlines() assert lines[0] == "---", f"{path} has no frontmatter" fields: dict[str, str | list[str]] = {} key = "" @@ -74,4 +79,4 @@ def test_claude_subagents_preload_shared_skills(agent: pathlib.Path) -> None: def test_claude_md_forwards_to_agents_md() -> None: - assert (ROOT / "CLAUDE.md").read_text().strip() == "@AGENTS.md" + assert (ROOT / "CLAUDE.md").read_text(encoding="utf-8").strip() == "@AGENTS.md" diff --git a/tests/test_docs.py b/tests/test_docs.py new file mode 100644 index 0000000..d06cbf9 --- /dev/null +++ b/tests/test_docs.py @@ -0,0 +1,114 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + +"""Check public documentation against its executable sources of truth.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +import inspect +import pathlib +import re +import subprocess +import sys +import tomllib + +import pytest + +import array_vectorize +from array_vectorize.frontend import tables + +ROOT = pathlib.Path(__file__).parents[1] + + +def _read_doc(name: str) -> str: + return (ROOT / "docs" / name).read_text(encoding="utf-8") + + +def _assert_identifiers_documented(document: str, identifiers: Iterable[str]) -> None: + """Require complete identifiers inside Markdown code spans.""" + documented = { + identifier + for span in re.findall(r"(? None: + """Compare the parameter table with runtime defaults, without pinning prose.""" + documented = dict( + re.findall(r"^\| `([a-z_]\w*)` \| `([^`]+)` \|", document, flags=re.MULTILINE) + ) + actual = {name: documented.get(name) for name in defaults} + assert actual == dict(defaults), f"Documented defaults {actual} differ from {dict(defaults)}" + + +def _assert_options_documented(document: str, help_text: str) -> None: + """Require every long CLI option to have an explicit code span.""" + options = set(re.findall(r"(? None: + _assert_identifiers_documented(_read_doc("api.md"), array_vectorize.__all__) + + +def test_vectorize_keyword_defaults_are_documented() -> None: + defaults = { + name: repr(parameter.default) + for name, parameter in inspect.signature(array_vectorize.vectorize).parameters.items() + if parameter.kind is inspect.Parameter.KEYWORD_ONLY + } + _assert_defaults_documented(_read_doc("api.md"), defaults) + + +def test_supported_mapping_identifiers_are_documented() -> None: + math_names = set(tables.MATH_FUNCS) | set(tables.MATH_SPECIAL) | set(tables.MATH_CONSTS) + builtin_names = ( + set(tables.BUILTIN_UNARY) | set(tables.BUILTIN_FOLDS) | set(tables.BUILTIN_CASTS) + ) + _assert_identifiers_documented( + _read_doc("supported-subset.md"), {f"math.{name}" for name in math_names} | builtin_names + ) + + +def test_fuzzer_options_are_documented() -> None: + result = subprocess.run( + [sys.executable, "-m", "array_vectorize.fuzz", "--help"], + check=True, + capture_output=True, + encoding="utf-8", + cwd=ROOT, + ) + _assert_options_documented(_read_doc("api.md"), result.stdout) + + +def test_python_requirement_is_documented() -> None: + metadata = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + requirement = metadata["project"]["requires-python"] + documented = re.search(r"Requires Python\s+([^\n.]+(?:\.\d+)*)\.", _read_doc("index.md")) + assert documented is not None, "Missing Python requirement" + assert documented[1].replace(" ", "") == requirement + + +@pytest.mark.parametrize("name", ["get_source", "math.sin", "int"]) +def test_identifier_check_rejects_missing_and_partial_names(name: str) -> None: + with pytest.raises(AssertionError, match="Undocumented identifiers"): + _assert_identifiers_documented(f"`{name}_extra` and prose {name}", [name]) + + +@pytest.mark.parametrize("document", ["", "| `strict` | `False` | changed default |"]) +def test_default_check_rejects_missing_and_changed_defaults(document: str) -> None: + with pytest.raises(AssertionError, match="Documented defaults"): + _assert_defaults_documented(document, {"strict": "True"}) + + +def test_cli_check_rejects_missing_and_partial_options() -> None: + with pytest.raises(AssertionError, match="Undocumented CLI options"): + _assert_options_documented("`--seed-extra`", "usage: tool --seed SEED") diff --git a/tests/test_hooks.py b/tests/test_hooks.py new file mode 100644 index 0000000..f0bf04d --- /dev/null +++ b/tests/test_hooks.py @@ -0,0 +1,48 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + +"""The hook runner installs both stages and preserves shared tool commands.""" + +from __future__ import annotations + +import os +import pathlib +import subprocess +import sys + +import yaml + +ROOT = pathlib.Path(__file__).parents[1] + + +def test_prek_installs_both_configured_hook_stages(tmp_path: pathlib.Path) -> None: + subprocess.run(["git", "init", "--quiet", str(tmp_path)], check=True) + (tmp_path / ".pre-commit-config.yaml").write_bytes( + (ROOT / ".pre-commit-config.yaml").read_bytes() + ) + old_hook = tmp_path / ".git" / "hooks" / "pre-commit" + old_hook.write_text("#!/bin/sh\nexit 1\n", encoding="utf-8", newline="\n") + prek = pathlib.Path(sys.executable).with_name("prek.exe" if os.name == "nt" else "prek") + subprocess.run([str(prek), "install", "--force"], cwd=tmp_path, check=True, capture_output=True) + for stage in ("pre-commit", "commit-msg"): + hook = tmp_path / ".git" / "hooks" / stage + text = hook.read_text(encoding="utf-8") + assert "prek" in text + assert stage in text + assert not hook.with_name(stage + ".legacy").exists() + + +def test_hooks_share_locked_tools_and_make_type_targets() -> None: + config = yaml.safe_load((ROOT / ".pre-commit-config.yaml").read_text(encoding="utf-8")) + hooks = {hook["id"]: hook for repo in config["repos"] for hook in repo["hooks"]} + assert config["default_install_hook_types"] == ["pre-commit", "commit-msg"] + assert hooks["license-headers"]["entry"] == "make headers" + assert hooks["license-headers"]["always_run"] is True + for checker in ("mypy", "pyright", "zuban", "pyrefly"): + assert hooks[checker]["entry"] == f"make type-{checker}" + assert hooks[checker]["pass_filenames"] is False + for name in ("cleanporter-fix", "ruff-check", "ruff-format"): + assert hooks[name]["entry"].startswith("uv run --frozen ") + assert hooks["conventional-pre-commit"]["stages"] == ["commit-msg"] diff --git a/tests/test_license_headers.py b/tests/test_license_headers.py new file mode 100644 index 0000000..bda7a80 --- /dev/null +++ b/tests/test_license_headers.py @@ -0,0 +1,354 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + +"""License enforcement covers repository-owned code and preserves source bytes.""" + +from __future__ import annotations + +import ast +import pathlib +import shutil +import subprocess +import sys +import tomllib + +import pytest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "license_headers.py" +HEADER = (ROOT / ".license-header.txt").read_bytes() + b"\n" + + +def _git(root: pathlib.Path, *args: str) -> None: + subprocess.run(["git", *args], cwd=root, capture_output=True, check=True) + + +@pytest.fixture +def repository(tmp_path: pathlib.Path) -> pathlib.Path: + script = tmp_path / "scripts" / "license_headers.py" + script.parent.mkdir(parents=True) + shutil.copyfile(SCRIPT, script) + shutil.copyfile(ROOT / ".license-header.txt", tmp_path / ".license-header.txt") + _git(tmp_path, "init", "--quiet") + # The copied utility is itself part of the checked repository. + assert _run(tmp_path, "--fix").returncode == 0 + return tmp_path + + +def _run(root: pathlib.Path, *args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(root / "scripts" / "license_headers.py"), *args], + cwd=root / "scripts", + capture_output=True, + text=True, + encoding="utf-8", + ) + + +@pytest.mark.parametrize( + ("preamble", "body"), + [ + (b"", b""), + (b"", b"x = 1"), + (b"", b"\n\nx = 1\n"), + (b"", b"\r\n\r\nx = 1\r\n"), + (b"", b"\n\n"), + (b"#!/usr/bin/env python\n", b"\n\nx = 1\n"), + ( + b"", + b'"""Module documentation."""\nfrom __future__ import annotations\nx = "caf\xc3\xa9"\n', + ), + (b"#!/usr/bin/env python\n", b"print('hello')\n"), + (b"# coding: latin-1\n", b"x = 'caf\xe9'\n"), + (b"#!/usr/bin/python\n# coding: latin-1\n", b"x = 'caf\xe9'\n"), + (b"# an unrelated comment\n# coding: latin-1\n", b"x = 'caf\xe9'\n"), + (b"\xef\xbb\xbf", b"x = 1\n"), + (b"#!/usr/bin/python\r\n", b"x = 1\r\n"), + ], +) +def test_fix_preserves_source_and_is_idempotent( + repository: pathlib.Path, preamble: bytes, body: bytes +) -> None: + path = repository / "a file.py" + raw = preamble + body + path.write_bytes(raw) + before = ast.dump(ast.parse(raw)) + checked = _run(repository, "--check") + assert checked.returncode == 1 + assert "a file.py" in checked.stdout + assert path.read_bytes() == raw + assert _run(repository, "--fix").returncode == 0 + header = HEADER.replace(b"\n", b"\r\n") if b"\r\n" in raw else HEADER + expected = preamble + header + body + assert path.read_bytes() == expected + assert ast.dump(ast.parse(expected)) == before + assert _run(repository, "--check").returncode == 0 + assert _run(repository, "--fix").returncode == 0 + assert path.read_bytes() == expected + + +@pytest.mark.parametrize( + "old", + [ + HEADER + HEADER, + HEADER.rstrip(b"\n") + b"\n", + HEADER.split(b"\n", 1)[1], + b"# array-vectorize: Previous project description.\n" + HEADER.split(b"\n", 1)[1], + HEADER.replace(b"2026", b"2025"), + HEADER.replace(b"Copyright (c)", b"copyright (c)").replace(b"# SPDX", b"#SPDX"), + HEADER.replace(b"See LICENSE for the full license text.", b"See LICENSE for license text."), + b"# Copyright (c) 2026 grAItools\n", + ], +) +def test_normalize_owned_headers(repository: pathlib.Path, old: bytes) -> None: + path = repository / "example.py" + body = b"# Preserve this comment.\nx = 1\n" + path.write_bytes(old + body) + assert _run(repository, "--check").returncode == 1 + assert path.read_bytes() == old + body + assert _run(repository, "--fix").returncode == 0 + assert path.read_bytes() == HEADER + body + assert _run(repository, "--check").returncode == 0 + + +@pytest.mark.parametrize( + "notice", + [ + b"# Copyright (c) 2026 Someone Else\n", + b"# SPDX-License-Identifier: Apache-2.0\n", + b"# Copyright (c) 2026 grAItools\n# SPDX-License-Identifier: MIT\n", + b"# Copyright (c) 2026 grAItools\n# SPDX-License-Identifier: Apache-2.0\n", + b"# Copyright (c) 2026 grAItools\n# Copyright (c) 2026 Someone Else\n", + b"# Licensed under Apache-2.0\n", + b"# This file is licensed under GPL-3.0-only\n", + b"# License: Apache-2.0\n", + ], +) +def test_conflicting_notices_are_never_overwritten(repository: pathlib.Path, notice: bytes) -> None: + path = repository / "conflict.py" + raw = notice + b"x = 1\n" + path.write_bytes(raw) + for mode in ("--check", "--fix"): + result = _run(repository, mode) + assert result.returncode == 1 + assert "conflict.py" in result.stderr + assert path.read_bytes() == raw + + +def test_git_scope_includes_hidden_untracked_and_fixtures(repository: pathlib.Path) -> None: + ignored = repository / "ignored.py" + ignored.write_bytes(b"x = 1\n") + (repository / ".gitignore").write_text("ignored.py\n", encoding="utf-8", newline="\n") + fixtures = repository / "tests" / "fixtures" + fixtures.mkdir(parents=True) + fixture = fixtures / "empty.py" + fixture.touch() + _git(repository, "add", "tests/fixtures/empty.py") + hidden = repository / ".github" / "new.py" + hidden.parent.mkdir() + hidden.touch() + stub = repository / "module.pyi" + stub.write_bytes(b"x: int\n") + deleted = repository / "deleted.py" + deleted.touch() + _git(repository, "add", "deleted.py") + deleted.unlink() + assert _run(repository, "--fix").returncode == 0 + assert fixture.read_bytes() == HEADER + assert hidden.read_bytes() == HEADER + assert stub.read_bytes() == HEADER + b"x: int\n" + assert ignored.read_bytes() == b"x = 1\n" + + +def test_misplaced_notices_are_rejected_and_embedded_inputs_are_untouched( + repository: pathlib.Path, +) -> None: + path = repository / "misplaced.py" + raw = b"x = 1\n" + HEADER + path.write_bytes(raw) + for mode in ("--check", "--fix"): + assert _run(repository, mode).returncode == 1 + assert path.read_bytes() == raw + embedded = b'input_code = """\n' + HEADER + b'x = 1\n"""\n' + path.write_bytes(embedded) + assert _run(repository, "--fix").returncode == 0 + assert path.read_bytes() == HEADER + embedded + assert _run(repository, "--check").returncode == 0 + + +def test_unrelated_copyright_comment_is_preserved(repository: pathlib.Path) -> None: + path = repository / "comment.py" + raw = b"# Verify copyright headers in our tests.\nx = 1\n" + path.write_bytes(raw) + assert _run(repository, "--fix").returncode == 0 + assert path.read_bytes() == HEADER + raw + + +def test_standalone_project_description_is_preserved(repository: pathlib.Path) -> None: + path = repository / "description.py" + raw = HEADER.split(b"\n", 1)[0] + b"\nx = 1\n" + path.write_bytes(raw) + for _ in range(2): + assert _run(repository, "--fix").returncode == 0 + assert path.read_bytes() == HEADER + raw + assert _run(repository, "--check").returncode == 0 + + +@pytest.mark.parametrize("newline", [b"\n", b"\r\n"]) +@pytest.mark.parametrize("existing_header", [False, True]) +@pytest.mark.parametrize( + "body", + [ + b"# See LICENSE for details\nx = 1\n", + b"# See LICENSE for the full license text.\nx = 1\n", + b"# Module explanation.\n# See LICENSE for details\n\nx = 1\n", + ], +) +def test_license_references_outside_owned_header_are_preserved( + repository: pathlib.Path, newline: bytes, existing_header: bool, body: bytes +) -> None: + path = repository / "reference.py" + header = HEADER.replace(b"\n", newline) + body = body.replace(b"\n", newline) + raw = (header if existing_header else b"") + body + path.write_bytes(raw) + assert _run(repository, "--check").returncode == (0 if existing_header else 1) + assert path.read_bytes() == raw + for _ in range(2): + assert _run(repository, "--fix").returncode == 0 + assert path.read_bytes() == header + body + assert _run(repository, "--check").returncode == 0 + + +def test_source_symlinks_are_rejected(repository: pathlib.Path, tmp_path: pathlib.Path) -> None: + target = tmp_path / "outside.txt" + target.write_bytes(b"x = 1\n") + link = repository / "linked.py" + try: + link.symlink_to(target) + except OSError as exc: + pytest.fail(f"symlink support is required: {exc}") + for mode in ("--check", "--fix"): + result = _run(repository, mode) + assert result.returncode == 1 + assert "symlink" in result.stderr + assert target.read_bytes() == b"x = 1\n" + + +def test_bad_template_and_git_errors_are_operational(repository: pathlib.Path) -> None: + template = repository / ".license-header.txt" + original = template.read_bytes() + template.write_bytes(b"not a comment\n") + assert _run(repository, "--check").returncode == 2 + template.write_bytes(original) + shutil.rmtree(repository / ".git") + assert _run(repository, "--check").returncode == 2 + + +def test_symlinked_parent_directories_are_never_followed( + repository: pathlib.Path, tmp_path: pathlib.Path +) -> None: + directory = repository / "src" + directory.mkdir() + path = directory / "module.py" + path.write_bytes(b"x = 1\n") + _git(repository, "add", "src/module.py") + path.unlink() + directory.rmdir() + outside = tmp_path.parent / (tmp_path.name + "-outside") + outside.mkdir() + target = outside / "module.py" + target.write_bytes(b"x = 1\n") + directory.symlink_to(outside, target_is_directory=True) + for mode in ("--check", "--fix"): + result = _run(repository, mode) + assert result.returncode == 1 + assert "symlink" in result.stderr + assert target.read_bytes() == b"x = 1\n" + + +def test_unknown_source_encoding_is_operational(repository: pathlib.Path) -> None: + path = repository / "bad.py" + raw = b"# coding: nonexistent-encoding\nx = 1\n" + path.write_bytes(raw) + assert _run(repository, "--fix").returncode == 2 + assert path.read_bytes() == raw + + +def test_fix_preserves_file_permissions(repository: pathlib.Path) -> None: + path = repository / "executable.py" + path.write_bytes(b"x = 1\n") + path.chmod(0o755) + original = path.stat().st_mode + assert _run(repository, "--fix").returncode == 0 + assert path.stat().st_mode == original + + +def test_shebang_without_a_final_newline(repository: pathlib.Path) -> None: + path = repository / "script.py" + path.write_bytes(b"#!/usr/bin/env python") + assert _run(repository, "--fix").returncode == 0 + expected = b"#!/usr/bin/env python\n" + HEADER + assert path.read_bytes() == expected + assert _run(repository, "--check").returncode == 0 + + +def test_notice_before_an_encoding_cookie_is_rejected(repository: pathlib.Path) -> None: + path = repository / "misplaced.py" + raw = b"# Copyright (c) 2026 grAItools\n# coding: utf-8\nx = 1\n" + path.write_bytes(raw) + for mode in ("--check", "--fix"): + assert _run(repository, mode).returncode == 1 + assert path.read_bytes() == raw + + +def test_cli_requires_exactly_one_mode(repository: pathlib.Path) -> None: + assert _run(repository).returncode == 2 + assert _run(repository, "--check", "--fix").returncode == 2 + + +def test_repository_headers_are_canonical() -> None: + result = _run(ROOT, "--check") + assert result.returncode == 0, result.stdout + result.stderr + + +def test_all_type_checkers_cover_the_header_utility() -> None: + config = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))["tool"] + for checker, key in ( + ("mypy", "files"), + ("pyright", "include"), + ("zuban", "files"), + ("pyrefly", "project-includes"), + ): + assert "scripts" in config[checker][key] + assert config["pyrefly"]["disable-project-excludes-heuristics"] is True + + +def test_header_hook_checks_the_whole_tree_even_on_template_only_changes() -> None: + # PyYAML is a dev dependency, independent of libcst's optional YAML backend. + code = ( + "import json, sys, yaml\n" + "with open(sys.argv[1], encoding='utf-8') as f:\n" + " json.dump(yaml.safe_load(f), sys.stdout)\n" + ) + import json + + result = subprocess.run( + [sys.executable, "-c", code, str(ROOT / ".pre-commit-config.yaml")], + capture_output=True, + text=True, + check=True, + ) + config = json.loads(result.stdout) + [hook] = [ + hook + for repo in config["repos"] + for hook in repo["hooks"] + if hook["id"] == "license-headers" + ] + assert hook["entry"] == "make headers" + assert hook["language"] == "system" + assert hook["always_run"] is True + assert hook["pass_filenames"] is False diff --git a/tests/test_portability.py b/tests/test_portability.py new file mode 100644 index 0000000..d0a5492 --- /dev/null +++ b/tests/test_portability.py @@ -0,0 +1,78 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + +"""Source inspection and repository text I/O across platforms.""" + +from __future__ import annotations + +import ast +import importlib.util +import inspect +import pathlib + +import numpy as np +import pytest +import support + +import array_vectorize + +ROOT = pathlib.Path(__file__).parents[1] + + +@pytest.mark.parametrize("newline", ["\n", "\r\n"], ids=["lf", "crlf"]) +def test_unicode_source_and_path_are_inspectable_and_vectorizable( + tmp_path: pathlib.Path, newline: str +) -> None: + directory = tmp_path / "données 日本語" + directory.mkdir() + path = directory / "résumé.py" + source = 'def subject(x):\n """Résumé: 日本語."""\n return x + 2.0\n' + path.write_bytes(source.replace("\n", newline).encode("utf-8")) + spec = importlib.util.spec_from_file_location("portability_subject", path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + assert inspect.getsource(module.subject) == source + vec = array_vectorize.vectorize(module.subject) + values = np.asarray([-1.0, 0.0, 1.0]) + np.testing.assert_array_equal(vec(values), [module.subject(float(value)) for value in values]) + assert "Résumé: 日本語." in support.with_metadata(vec).source + + +def test_snippet_helpers_write_utf8_and_lf( + tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(support, "TMPDIR", tmp_path) + support.make_fn(' """Résumé: 日本語."""\n return x + y') + support.make_module('def subject(x):\n """Résumé: 日本語."""\n return x\n') + paths = list(tmp_path.glob("*.py")) + assert len(paths) == 2 + for path in paths: + raw = path.read_bytes() + assert "Résumé: 日本語." in raw.decode("utf-8") + assert b"\r" not in raw + + +def test_repository_path_text_operations_have_explicit_utf8() -> None: + failures = [] + for directory in ("src", "tests", "scripts"): + for path in (ROOT / directory).rglob("*.py"): + if "__pycache__" in path.parts: + continue + tree = ast.parse(path.read_text(encoding="utf-8")) + for node in ast.walk(tree): + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr in {"read_text", "write_text"} + ): + encoding = next( + (keyword.value for keyword in node.keywords if keyword.arg == "encoding"), + None, + ) + if not isinstance(encoding, ast.Constant) or encoding.value != "utf-8": + failures.append(f"{path.relative_to(ROOT)}:{node.lineno}") + assert not failures, f"Path text operations need explicit UTF-8: {failures}" diff --git a/tests/test_release.py b/tests/test_release.py new file mode 100644 index 0000000..fb04dc1 --- /dev/null +++ b/tests/test_release.py @@ -0,0 +1,212 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + +"""Release metadata, checkout safety, and artifact smoke orchestration.""" + +from __future__ import annotations + +import importlib.util +import pathlib +import subprocess +import types +import typing + +import pytest +import yaml + +ROOT = pathlib.Path(__file__).resolve().parents[1] + + +def _load(name: str) -> types.ModuleType: + spec = importlib.util.spec_from_file_location(name, ROOT / "scripts" / f"{name}.py") + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +release = _load("release") +package_smoke = _load("package_smoke") + + +@pytest.fixture +def project(tmp_path: pathlib.Path) -> pathlib.Path: + (tmp_path / "src/array_vectorize").mkdir(parents=True) + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "array-vectorize"\nversion = "0.1.0"\n', encoding="utf-8" + ) + (tmp_path / "src/array_vectorize/__init__.py").write_text( + '__version__ = "0.1.0"\n', encoding="utf-8" + ) + (tmp_path / "uv.lock").write_text( + '[[package]]\nname = "array-vectorize"\nversion = "0.1.0"\n', encoding="utf-8" + ) + (tmp_path / "CHANGELOG.md").write_text("## [0.1.0] - 2026-10-06\n", encoding="utf-8") + return tmp_path + + +@pytest.mark.parametrize("tag", ["v0.1.0", "v1!2.0rc1.post2.dev3", "v1.0.post1", "v1.0.dev0"]) +def test_normalized_public_versions(tag: str) -> None: + assert release.version_from_tag(tag) == tag[1:] + + +@pytest.mark.parametrize( + "tag", ["1.0", "v", "v01.0", "v1.0+local", "v1.0-rc1", "v1.0RC1", "v1.0rc", "v0!1.0"] +) +def test_release_tags_reject_aliases_and_local_versions(tag: str) -> None: + with pytest.raises(ValueError, match="invalid release tag"): + release.version_from_tag(tag) + + +def test_versions_and_changelog_match(project: pathlib.Path) -> None: + assert release.validate_metadata(project, "v0.1.0") == "0.1.0" + + +@pytest.mark.parametrize( + ("filename", "text", "message"), + [ + ("pyproject.toml", '[project]\nname="array-vectorize"\nversion="0.2.0"', "pyproject"), + ("src/array_vectorize/__init__.py", '__version__="0.2.0"', "source"), + ("uv.lock", '[[package]]\nname="array-vectorize"\nversion="0.2.0"', "uv.lock"), + ("CHANGELOG.md", "## [Unreleased]\n", "CHANGELOG"), + ], +) +def test_metadata_mismatch_rejected( + project: pathlib.Path, filename: str, text: str, message: str +) -> None: + (project / filename).write_text(text, encoding="utf-8") + with pytest.raises(ValueError, match=message): + release.validate_metadata(project, "v0.1.0") + + +@pytest.mark.parametrize( + ("version", "expected"), + [("1.0", False), ("1.0.post1", False), ("1.0rc1", True), ("1.0.dev1", True)], +) +def test_prerelease_detection(version: str, expected: bool) -> None: + assert release.is_prerelease(version) is expected + + +def _git(root: pathlib.Path, *arguments: str) -> None: + subprocess.run(["git", *arguments], cwd=root, check=True, capture_output=True) + + +@pytest.fixture +def checkout(project: pathlib.Path) -> pathlib.Path: + _git(project, "init") + _git(project, "config", "user.email", "test@example.com") + _git(project, "config", "user.name", "Test") + _git(project, "add", ".") + _git(project, "commit", "-m", "initial") + _git(project, "update-ref", "refs/remotes/origin/main", "HEAD") + return project + + +def test_clean_checkout(checkout: pathlib.Path) -> None: + release.check_clean(checkout) + + +@pytest.mark.parametrize("filename", ["CHANGELOG.md", "new.py"]) +def test_dirty_checkout_is_rejected(checkout: pathlib.Path, filename: str) -> None: + (checkout / filename).write_text("changed\n", encoding="utf-8") + with pytest.raises(ValueError, match="dirty"): + release.check_clean(checkout) + + +def test_commit_outside_main_is_rejected(checkout: pathlib.Path) -> None: + _git(checkout, "commit", "--allow-empty", "-m", "outside main") + with pytest.raises(ValueError, match="ancestor"): + release.validate_checkout(checkout) + + +def test_stale_lock_failure_is_propagated( + checkout: pathlib.Path, monkeypatch: pytest.MonkeyPatch +) -> None: + original = subprocess.run + + def run(command: list[str], **kwargs: typing.Any) -> subprocess.CompletedProcess[str]: + if command == ["uv", "lock", "--check"]: + raise subprocess.CalledProcessError(1, command) + return original(command, **kwargs) + + monkeypatch.setattr(subprocess, "run", run) + with pytest.raises(subprocess.CalledProcessError): + release.validate_checkout(checkout) + + +def test_release_workflow_validates_and_smokes_before_upload() -> None: + workflow = yaml.safe_load((ROOT / ".github/workflows/release.yml").read_text(encoding="utf-8")) + assert workflow["permissions"] == {"contents": "read"} + assert workflow["concurrency"]["cancel-in-progress"] is False + build = workflow["jobs"]["build"] + steps = build["steps"] + commands = [step["run"] for step in steps if "run" in step] + assert commands.index( + 'uv run --frozen python scripts/release.py --tag "$GITHUB_REF_NAME"' + ) < commands.index("make check") + assert commands.index( + "uv run --frozen python scripts/release.py --check-clean" + ) < commands.index("uv build --no-sources") + assert commands.index("uv build --no-sources") < commands.index( + 'make package-smoke VERSION="${GITHUB_REF_NAME#v}"' + ) + assert next(step for step in steps if step.get("run") == "make check")["env"]["CI"] == "1" + assert "upload-artifact" in steps[-1]["uses"] + publish = workflow["jobs"]["publish"] + assert publish["needs"] == "build" + assert publish["permissions"] == {"contents": "write"} + assert "download-artifact" in publish["steps"][0]["uses"] + assert "--verify-tag" in publish["steps"][1]["run"] + + +def test_artifacts_reject_stale_or_missing_distributions(tmp_path: pathlib.Path) -> None: + with pytest.raises(ValueError, match="exactly one"): + package_smoke.artifacts(tmp_path) + (tmp_path / "package.whl").touch() + (tmp_path / "package.tar.gz").touch() + assert len(package_smoke.artifacts(tmp_path)) == 2 + (tmp_path / ".gitignore").write_bytes(b"*") + assert len(package_smoke.artifacts(tmp_path)) == 2 + (tmp_path / "stale.whl").touch() + with pytest.raises(ValueError, match="exactly one"): + package_smoke.artifacts(tmp_path) + + +def test_artifact_smoke_uses_isolated_environment( + tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch +) -> None: + calls: list[tuple[list[str], dict[str, typing.Any]]] = [] + + def run(command: list[str], **kwargs: typing.Any) -> subprocess.CompletedProcess[str]: + calls.append((command, kwargs)) + return subprocess.CompletedProcess(command, 0) + + monkeypatch.setattr(subprocess, "run", run) + monkeypatch.setenv("PYTHONPATH", str(ROOT / "src")) + package_smoke.smoke(tmp_path / "package.whl", "0.1.0") + assert len(calls) == 3 + assert calls[1][0][-2:] == [str(tmp_path / "package.whl"), "numpy>=2.0"] + assert calls[2][0][1] == "-I" + for _, kwargs in calls: + assert ROOT not in pathlib.Path(kwargs["cwd"]).parents + assert "PYTHONPATH" not in kwargs["env"] + + +def test_artifact_install_failure_prevents_smoke( + tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch +) -> None: + calls: list[list[str]] = [] + + def run(command: list[str], **kwargs: typing.Any) -> subprocess.CompletedProcess[str]: + calls.append(command) + if command[1:3] == ["pip", "install"]: + raise subprocess.CalledProcessError(1, command) + return subprocess.CompletedProcess(command, 0) + + monkeypatch.setattr(subprocess, "run", run) + with pytest.raises(subprocess.CalledProcessError): + package_smoke.smoke(tmp_path / "package.whl", "0.1.0") + assert len(calls) == 2 diff --git a/tests/unit/test_builtins.py b/tests/unit/test_builtins.py index 37a183d..58cd89e 100644 --- a/tests/unit/test_builtins.py +++ b/tests/unit/test_builtins.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Tests for the mapping tables: every target must exist on array-api-strict.""" from __future__ import annotations diff --git a/tests/unit/test_codegen.py b/tests/unit/test_codegen.py index 295dcc1..d1560ce 100644 --- a/tests/unit/test_codegen.py +++ b/tests/unit/test_codegen.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Codegen tests: IR -> source text via ast.unparse.""" from __future__ import annotations diff --git a/tests/unit/test_docstring.py b/tests/unit/test_docstring.py index 7ff8219..cc490c3 100644 --- a/tests/unit/test_docstring.py +++ b/tests/unit/test_docstring.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Docstring builder tests: prefixing, style detection, Notes merge, round trip.""" from __future__ import annotations diff --git a/tests/unit/test_extract.py b/tests/unit/test_extract.py index 1be83af..f9b8d0e 100644 --- a/tests/unit/test_extract.py +++ b/tests/unit/test_extract.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Tests for source extraction and closure capture.""" from __future__ import annotations @@ -249,7 +254,9 @@ def test_lambda_probe_fallback_identifies_sibling() -> None: tmp = pathlib.Path(tempfile.mkdtemp()) path = tmp / "probe_siblings.py" - path.write_text("f1, f2 = (lambda x: x + 1), (lambda x: x + 2)\n") + path.write_text( + "f1, f2 = (lambda x: x + 1), (lambda x: x + 2)\n", encoding="utf-8", newline="\n" + ) spec = importlib.util.spec_from_file_location(path.stem, path) assert spec is not None assert spec.loader is not None @@ -280,7 +287,9 @@ def test_lambda_probe_fallback_interchangeable() -> None: tmp = pathlib.Path(tempfile.mkdtemp()) path = tmp / "probe_twin.py" - path.write_text("f1, f2 = (lambda x: x + 1), (lambda x: x + 1)\n") + path.write_text( + "f1, f2 = (lambda x: x + 1), (lambda x: x + 1)\n", encoding="utf-8", newline="\n" + ) spec = importlib.util.spec_from_file_location(path.stem, path) assert spec is not None assert spec.loader is not None @@ -303,7 +312,11 @@ def test_lambda_probe_fallback_rejects_unidentifiable() -> None: tmp = pathlib.Path(tempfile.mkdtemp()) path = tmp / "probe_none.py" - path.write_text("f1, f2 = (lambda x: x + 1), (lambda x: x + 2)\nf3 = lambda x: x + 3\n") + path.write_text( + "f1, f2 = (lambda x: x + 1), (lambda x: x + 2)\nf3 = lambda x: x + 3\n", + encoding="utf-8", + newline="\n", + ) spec = importlib.util.spec_from_file_location(path.stem, path) assert spec is not None assert spec.loader is not None diff --git a/tests/unit/test_fuzz.py b/tests/unit/test_fuzz.py index 28a25b7..1733497 100644 --- a/tests/unit/test_fuzz.py +++ b/tests/unit/test_fuzz.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Fuzzer smoke test: small, seeded, fast.""" from __future__ import annotations diff --git a/tests/unit/test_ir.py b/tests/unit/test_ir.py index 56834b4..dd9066c 100644 --- a/tests/unit/test_ir.py +++ b/tests/unit/test_ir.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Tests for the IR: node equality/hashability, is_bool lattice, SSA naming.""" from __future__ import annotations diff --git a/tests/unit/test_layering.py b/tests/unit/test_layering.py index 9473d3d..4fb0d65 100644 --- a/tests/unit/test_layering.py +++ b/tests/unit/test_layering.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Import-layering rules of the package, checked statically. These encode the boundaries described in docs/architecture.md so a move @@ -84,7 +89,7 @@ def _classify(tree: ast.Module, package_dir: list[str]) -> tuple[set[str], set[s def _imports(path: pathlib.Path) -> tuple[set[str], set[str]]: """(package units, external top-level modules) imported by ``path``.""" package_dir = list(path.relative_to(ROOT).parts[:-1]) - return _classify(ast.parse(path.read_text()), package_dir) + return _classify(ast.parse(path.read_text(encoding="utf-8")), package_dir) SOURCES = sorted(ROOT.rglob("*.py")) diff --git a/tests/unit/test_lower.py b/tests/unit/test_lower.py index facc6c2..dcdddc4 100644 --- a/tests/unit/test_lower.py +++ b/tests/unit/test_lower.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Lowering tests: AST -> IR shapes, straight-line + control flow.""" from __future__ import annotations diff --git a/tests/unit/test_optimize.py b/tests/unit/test_optimize.py index 0caa549..62e069c 100644 --- a/tests/unit/test_optimize.py +++ b/tests/unit/test_optimize.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Optimizer tests: const-fold, DCE, CSE.""" from __future__ import annotations diff --git a/tests/unit/test_runtime.py b/tests/unit/test_runtime.py index d8f8914..662ee90 100644 --- a/tests/unit/test_runtime.py +++ b/tests/unit/test_runtime.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Direct unit tests for the call-time runtime library. runtime/arith, runtime/minmax and runtime/dtype are normally exercised diff --git a/tests/unit/test_validate.py b/tests/unit/test_validate.py index 15bbc70..2e1f3fd 100644 --- a/tests/unit/test_validate.py +++ b/tests/unit/test_validate.py @@ -1,3 +1,8 @@ +# array-vectorize: compile scalar Python functions into exact Array API functions. +# Copyright (c) 2026 grAItools +# SPDX-License-Identifier: BSD-3-Clause +# See LICENSE for the full license text. + """Validator tests: accept/reject per construct, linter-style diagnostics.""" from __future__ import annotations diff --git a/uv.lock b/uv.lock index 991966c..1450767 100644 --- a/uv.lock +++ b/uv.lock @@ -67,12 +67,13 @@ dev = [ { name = "hypothesis" }, { name = "mypy" }, { name = "numpy" }, - { name = "pre-commit" }, + { name = "prek" }, { name = "pyrefly" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-benchmark" }, { name = "pytest-timeout" }, + { name = "pyyaml" }, { name = "ruff" }, { name = "zuban" }, ] @@ -100,12 +101,13 @@ dev = [ { name = "hypothesis", specifier = ">=6.100" }, { name = "mypy", specifier = ">=1.20" }, { name = "numpy", specifier = ">=2.0" }, - { name = "pre-commit", specifier = ">=3.5" }, + { name = "prek", specifier = ">=0.5" }, { name = "pyrefly", specifier = ">=1.2" }, { name = "pyright", specifier = ">=1.1.411" }, { name = "pytest", specifier = ">=9.0" }, { name = "pytest-benchmark", specifier = ">=4.0" }, { name = "pytest-timeout", specifier = ">=2.3" }, + { name = "pyyaml", specifier = ">=6.0.1" }, { name = "ruff", specifier = ">=0.15" }, { name = "zuban", specifier = ">=0.9.2" }, ] @@ -265,15 +267,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, ] -[[package]] -name = "cfgv" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, -] - [[package]] name = "cleanporter" version = "0.6.0" @@ -408,15 +401,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/91/600003aaad107e27553fbc9cbfc57e96fa37e0223d2ef9d7d3a0e8d8d070/deepmerge-3.0.1-py3-none-any.whl", hash = "sha256:35c96f6a68fcf90719a5b31d9f8042ecef6c00fb56836660d33455d0f5cfda65", size = 14909, upload-time = "2026-09-01T14:09:43.364Z" }, ] -[[package]] -name = "distlib" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, -] - [[package]] name = "docutils" version = "0.23" @@ -529,15 +513,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/63/117c82f08ab3ba1dcfbf6562ac43b8deb8efa8106646494fadd15122cc1b/hypothesis-6.168.5-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:0600ddc24c32dab5ca8e780630ab6e2561df6d7f594f781d0608b38e04c4da91", size = 679632, upload-time = "2026-10-05T23:24:33.753Z" }, ] -[[package]] -name = "identify" -version = "2.6.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/35/d70c0006c7cee65999ea94a6273e60b2094f600a3d8b71b04318253fc643/identify-2.6.20.tar.gz", hash = "sha256:ad729860a923858d26917c2f4fb0a1d83d27a75b1e090c06440c573f048f3285", size = 99681, upload-time = "2026-09-26T20:29:07.187Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/70/fffc9613501877c0a10a1ef73a165e6ded3e53f6e7502227fef56d973933/identify-2.6.20-py2.py3-none-any.whl", hash = "sha256:6a16b69b93187244e0548cbfd25b3e4a6f9a7a2ad784625c3bec2b8d27b81aaa", size = 99506, upload-time = "2026-09-26T20:29:06.054Z" }, -] - [[package]] name = "idna" version = "3.20" @@ -1324,15 +1299,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] -[[package]] -name = "platformdirs" -version = "4.12.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/42/23/4a86fc741c38c5b69792a4ef954b281afa69bea9f083f881de1b0d23bc07/platformdirs-4.12.3.tar.gz", hash = "sha256:427fc0bb321ae0c5b037fa03238ca74820437be162e78b4848c4d4055b9b766c", size = 59538, upload-time = "2026-10-03T23:40:46.595Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/9b/6ce1ead737fe496611bda600c263b9a11ae7bd8f41bb13f9bd7e0a2c37a4/platformdirs-4.12.3-py3-none-any.whl", hash = "sha256:080f3b39423b5abfca9a23d84c4e9795f54d395cd8459867a8ded44084fcd5f8", size = 32493, upload-time = "2026-10-03T23:40:45.104Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -1343,19 +1309,20 @@ wheels = [ ] [[package]] -name = "pre-commit" -version = "4.6.2" +name = "prek" +version = "0.5.5" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/7b/11538b8e6cd7e269261feae62a2d08818fdcf8f43c3b1a253cec01a5488d/prek-0.5.5.tar.gz", hash = "sha256:16bf596768bb754779c7d42317e9ca16ac609c0cce7d13bae0829944ab40a26a", size = 570428, upload-time = "2026-10-05T06:08:59.165Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" }, + { url = "https://files.pythonhosted.org/packages/2e/77/189e554ea92baf6c1d33733b3e9d1ed819ab7d77e57eb2e3ead0bb1d8060/prek-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:50fbceb0f728b504023f6e6c9037ac0656436949d94f7a85bac5542e6c1a4a75", size = 6038669, upload-time = "2026-10-05T06:08:39.368Z" }, + { url = "https://files.pythonhosted.org/packages/63/ea/fc857c6b3f1e3fdb4d81bdca2051076de82e986cb1455a01b351ac7a5cd1/prek-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2f610927b95e84b28173a4c810892a7b5d5bb55bee792ddf97178fa97654633e", size = 5548955, upload-time = "2026-10-05T06:08:41.428Z" }, + { url = "https://files.pythonhosted.org/packages/47/c3/2bab54f6393fb9f247225681d476086e24fa6242f329e9ca1e5bd638ec50/prek-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:120ef7d4b6bb58155e74827743c266e48501efb0e4c37ff726b14fe6e86a126b", size = 5848228, upload-time = "2026-10-05T06:08:44.051Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f9/1279e1c83819de5904db1a09f4afc589932a2e62c7063713b1fe3a54ee46/prek-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f6c453a12c21757b23bbbc97415aef78527841ad3f9e61f52cb35cf012f93e5", size = 6371296, upload-time = "2026-10-05T06:08:46.278Z" }, + { url = "https://files.pythonhosted.org/packages/93/fc/e566be5919eaffef4cc22488f16e4bbfd08961734bd67c06f85d405e4cbc/prek-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73d509c0d9556171de2502bada4962be7a25871e7b23e782549074e1416dc850", size = 6269922, upload-time = "2026-10-05T06:08:48.645Z" }, + { url = "https://files.pythonhosted.org/packages/d2/39/a48d8e6203055bff7bb734b1f2f9aede4a4539bee6ab4c9273dbc01fa546/prek-0.5.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:76c867fdbd4e4f19e6b6b2988bd1e1407d1324170ebcf1428e4e34f8f0751a99", size = 5861518, upload-time = "2026-10-05T06:08:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/b7/03/690fba9543033e25ea2e7b2ae1cca1ae8d8c2e32295f0d66f4b50934b8c7/prek-0.5.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:c69f60c2b3b4d481e38d0eab3b97ac05ec05d7648ae690925ae95ca9d22c7643", size = 6383685, upload-time = "2026-10-05T06:08:52.868Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/1e9ccf3a61a3bfd4107b83973f7afde9177b6ff8f2d5d4b00125e77c2100/prek-0.5.5-py3-none-win_amd64.whl", hash = "sha256:15bff9039349466ea9bf6b0c173b822ff08f8ae4cfbfdea782492e519c06d874", size = 5784759, upload-time = "2026-10-05T06:08:55.056Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/bb6811e438d3c74d26cb5d72e6343606e0a7a6283fee87521189f4ba1191/prek-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3db3191d2509830c75438e20956a47c2259867d18a4106064ed75088b968a0c6", size = 5517355, upload-time = "2026-10-05T06:08:57.193Z" }, ] [[package]] @@ -1499,18 +1466,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] -[[package]] -name = "python-discovery" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0c/57/250bd238b966cece44328235eb85290045d059265fdaf7527a3a958123db/python_discovery-1.6.1.tar.gz", hash = "sha256:cf87d3627dfb4412437fdd5b13eae402607722998d21567993aedbc59b23c15e", size = 84338, upload-time = "2026-09-18T01:31:53.971Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/7d/e9ffbadfbf89c93848412d04594135c4ae8c1d37d9e053b9c3ed718fabc4/python_discovery-1.6.1-py3-none-any.whl", hash = "sha256:d43fcdef879fe795352bd13ccf8d185ba5a9f86f36cfcd00529f596e737442b3", size = 38664, upload-time = "2026-09-18T01:31:52.448Z" }, -] - [[package]] name = "python-multipart" version = "0.0.32" @@ -1927,22 +1882,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" }, ] -[[package]] -name = "virtualenv" -version = "21.14.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "packaging" }, - { name = "platformdirs" }, - { name = "python-discovery" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/f9/f323b3b6058cff3853b31cf6a49c0425ed797bf9611572ec10d065e08bac/virtualenv-21.14.5.tar.gz", hash = "sha256:c4cb6c13e46b57225a999c7e22a09b163393878facc7ac4c059a57f46faa1647", size = 5467127, upload-time = "2026-10-02T17:42:25.613Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/93/084f10ebbecdedf7902ed68fbecc903f0e9c5742155bfffb05eccbc071da/virtualenv-21.14.5-py3-none-any.whl", hash = "sha256:b0651e0174982bba17cc6f0aaa2ef496730cb85a3f35eb5d1119d1c904f7d1da", size = 5487482, upload-time = "2026-10-02T17:42:23.38Z" }, -] - [[package]] name = "websockets" version = "17.2"