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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,44 @@ jobs:
- run: uv pip install --python .minimal/bin/python dist/*.whl "beartype==0.23.0rc0"
- run: .minimal/bin/python -I tools/smoke_minimal.py

installed:
name: Installed consumers (Python ${{ matrix.python }})
needs: distributions
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: ["3.10", "3.14"]
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ inputs.ref || github.sha }}
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v7
with:
version: "0.11.17"
enable-cache: true
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: candidate-distributions
path: dist/
- run: >-
uv run --locked --only-group dev python tools/check_installed.py
dist/*.whl dist/*.tar.gz --python "${{ matrix.python }}"

required:
name: Required validation
if: always()
needs:
[quality, runtime, checkers, compatibility, docs, distributions, minimal]
[
quality,
runtime,
checkers,
compatibility,
docs,
distributions,
minimal,
installed,
]
runs-on: ubuntu-latest
steps:
- name: Require every validation job to succeed
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project follows

## [Unreleased]

- Require runtime and four-checker consumer tests from normally installed
sdist-derived wheels outside the checkout on Python 3.10 and 3.14.

- Share candidate validation across PRs, pushes and nightly runs, with explicit
CPU backends, four checkers, complete hooks and a required final gate.

Expand Down
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,16 @@ Use `uv run --locked prek run -a` for the normal hooks and
Pre-push checks use the locked four-checker harness and runtime suite. The
required CI gate accepts only successful completion of every required job. CuPy
GPU validation remains a separate hardware-backed requirement.

To validate the installed artifact after building the sdist and its wheel, run:

```bash
uv run --locked python tools/check_installed.py \
dist/*.whl dist/*.tar.gz --python 3.10
```

Repeat with `--python 3.14`. The command installs locked dependencies and the
wheel normally in a temporary environment, then runs copied runtime and checker
fixtures with no package source directory. It reports artifact hashes and
installed module origins. `--installed-package` is an explicit pytest mode for
this consumer check; ordinary source validation continues to include `src`.
88 changes: 88 additions & 0 deletions plans/2026-09-08-installed-consumers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Prove the normally installed candidate artifact


Maintain this ExecPlan according to `PLANS.md`. This focused artifact-consumer PR is stacked on CI PR #23 and implements the remaining installation proof in M8.

## Purpose / Big Picture


The package users install must pass the same runtime and static consumer contracts as the checkout. Build the wheel from the source archive, normally install it with exact beartype 0.23.0rc0, and run copied downstream tests outside every source checkout. Keep the resulting hashes and module origins traceable to the candidate commit.

## Progress


- [x] (2026-09-08) Created isolated branch/worktree from the combined CI candidate.
- [x] (2026-09-08) Opened draft PR #25 and inspected archive/test configuration.
- [x] (2026-09-08) Added explicit --installed-package mode; normal source targets remain unchanged.
- [x] (2026-09-08) Added normal wheel installation driver with copied archive fixtures, dependency lock and site-packages origin checks.
- [x] (2026-09-08) Initial isolated wheel runs each passed all 1,074 tests with five documented platform/CuPy skips.
- [x] (2026-09-08) Added required endpoint jobs consuming the same candidate-distributions artifact.
- [x] (2026-09-08) Final archive-owned-lock runs passed 1,074 tests with five documented skips at both endpoints; full hooks and actionlint pass.
- [x] (2026-09-08) Every required hosted job passed at 73b3ec9; PR #25 is ready for user review.
- [ ] Record hashes, origins and results; obtain user validation before merge.

## Surprises & Discoveries


Source tests currently assume a src directory in the checker target list. The archive already ships tests and configuration, but copied installed-package checks must omit that source target explicitly. The CI work exposed a hard-coded pyright venv override that could conceal the actual environment; its fix is present in this base.

## Decision Log


Decision: Add an explicit pytest installed-package option rather than infer mode from a missing source directory. Rationale: an accidental missing source tree must not silently reduce normal validation. Date: 2026-09-08.

Decision: Copy only downstream tests/configuration from the source archive into a temporary consumer directory, leaving package source absent. Rationale: source imports can mask incomplete wheels and missing typing markers. Install the wheel normally, with declared dependencies and exact beartype; never bypass resolution with --no-deps.

Decision: Reuse the candidate-distributions artifact from the shared build job. Rationale: consumer validation and subsequent publication must refer to the same bytes. Keep publication changes in a separate PR.

## Outcomes & Retrospective


The final normally installed wheel passes all 1,074 runtime/checker tests on Python 3.10.20 and 3.14.5. Both environments import bearshape from site-packages with exact beartype 0.23.0rc0, no editable project and no copied src directory. Five skips are CuPy absence and platform long-double distinctions. Hosted validation and user merge review remain.

## Context and Orientation


Worktree `/Users/ale/Code/bearshape-worktrees/installed-consumers`, branch `codex/installed-consumers`, base CI `f396e72`. `tools/check_distribution.py` checks license, metadata, package bytes and source-test inclusion. `tests/test_typecheck.py` runs four positive/negative batches. `tests/conftest.py` owns pytest filtering; `pytest.toml` configures the shared runner defaults. The shared workflow builds one sdist-derived wheel and makes it available as candidate-distributions.

## Plan of Work


Add --installed-package to pytest and make only the positive source/checker batch omit src in that explicit mode. Runtime and negative fixtures stay unchanged. Build and inspect archives, then create a fresh normal virtual environment for the selected interpreter. Export locked runtime/backend/checker/test dependencies without the editable project, install those dependencies and the actual wheel with exact beartype.

Extract downstream tests and required configuration into a temporary directory without src. Verify bearshape imports from the new environment's site-packages, no editable project is installed, and exact versions/hashes are reported. Run the full CPU runtime suite plus every checker against copied fixtures. Fail on missing tools, unexpected diagnostics or source origins. Keep optional CuPy skips explicit; separate GPU evidence remains required.

Wire endpoint consumer jobs to the existing distribution artifact and include them in the required gate. Save concise results and hashes. Run hooks and workflow lint, then inspect hosted results.

## Concrete Steps


From this worktree build sdist and then its wheel, using tools/check_distribution.py before installing. Use a separate temporary consumer root and normal uv pip installation. Run copied tests with:

python -I -m pytest tests/ --installed-package -n 4

Record actual checker imports and ensure their interpreter paths point to the consumer environment. Use the maintained command from CI at both endpoints.

## Validation and Acceptance


The consumer directory contains no src/bearshape. Normal wheel resolution permits exact beartype rc0. Runtime tests and all four checker batches pass under Python 3.10 and 3.14, with only documented absent-CuPy/platform skips. Report artifact SHA256 and installed module paths. The final CI gate requires both endpoint results and no rebuild occurs between artifact validation stages.

## Idempotence and Recovery


Use disposable directories and local virtual environments only. Do not modify system Python, main, release tags or ownership. Keep feature PRs separate and require user validation before merge.

## Artifacts and Notes


Evidence goes under `/Users/ale/Code/bearshape-implementation-2026-09-08/evidence/installed-*`. The final local wheel SHA256 is ef8d38266da7150efa80191589b7ba0310007a6ae2dbb82d30d4bfe4e0ed49cf; source archive SHA256 is e738d7ab34d74d7ea221f1db3aef291e37baf7ad38c5c9dfa7276735dc93c23a. Consumer logs are installed-final-3.10.log and installed-final-3.14.log; each reports its temporary site-packages origin. Hosted builds record their own hashes because each archive is tied to its source state. These results complement, but do not replace, exact GPU runtime tests or the unresolved native-union contract decision.

## Interfaces and Dependencies


No public runtime API or dependency change is planned. The pytest option is for downstream artifact validation. Use the existing locked backend/checker/test groups and standard-library archive/process tools for orchestration.

Revision note — 2026-09-08: Validated the final driver using the archive's own lockfile and required both installed-consumer jobs in CI.

Revision note — 2026-09-08: Recorded the completed hosted validation and retained user merge approval as the only remaining review action for this focused scope. Program-level release decisions remain in the roadmap and handoff report.
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
}


def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption(
"--installed-package",
action="store_true",
help="Check copied consumers against an installed wheel without source targets",
)


def pytest_collection_modifyitems(
config: pytest.Config, items: list[pytest.Item]
) -> None:
Expand Down
7 changes: 5 additions & 2 deletions tests/test_typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,11 @@ def _diagnostics(tool: str, output: str) -> Counter[tuple[str, int, str]]:

@pytest.mark.typecheck
@pytest.mark.parametrize("tool", SELECTED_CHECKERS)
def test_valid_consumers_and_source(tool: str) -> None:
result = _run(tool, ["src", "tests/typing"])
def test_valid_consumers_and_source(tool: str, pytestconfig: pytest.Config) -> None:
targets = ["tests/typing"]
if not pytestconfig.getoption("--installed-package"):
targets.insert(0, "src")
result = _run(tool, targets)
assert result.returncode == 0, (
f"{tool} rejected valid code on Python {PYTHON_TARGET}:\n{result.stdout}\n{result.stderr}"
)
Expand Down
3 changes: 3 additions & 0 deletions tools/check_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@
"ty.toml",
"ruff.toml",
"tools/check_distribution.py",
"tools/check_installed.py",
"tools/validate_runtime.py",
"tools/validate_tox_env.py",
"tests/conftest.py",
"tests/test_numpy.py",
"tests/test_tree.py",
"tests/test_typecheck.py",
"tests/typing/check_imports.py",
"tests/typing_negative/invalid_calls.py",
}


Expand Down
154 changes: 154 additions & 0 deletions tools/check_installed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Normally install a candidate wheel and test consumers outside the checkout."""

# Command driver: argument lists only, never a shell.
# ruff: noqa: S404, S603

from __future__ import annotations

import argparse
import json
import os
import shutil
import subprocess
import sys
import tarfile
import tempfile
from pathlib import Path

from check_distribution import check_distribution

_CONFIGS = {"pyproject.toml", "pytest.toml", "ty.toml", "uv.lock"}
_ORIGIN_CHECK = """
import importlib.metadata
import json
import pathlib
import sys
import bearshape
import beartype
origin = pathlib.Path(bearshape.__file__).resolve()
assert origin.is_relative_to(pathlib.Path(sys.prefix)), origin
assert beartype.__version__ == '0.23.0rc0', beartype.__version__
metadata = importlib.metadata.distribution('bearshape')
direct = json.loads(metadata.read_text('direct_url.json') or '{}')
assert not direct.get('dir_info', {}).get('editable'), direct
print(json.dumps({'python': sys.version, 'bearshape': bearshape.__version__,
'beartype': beartype.__version__, 'module': str(origin)}, indent=2))
"""


def _copy_consumers(sdist: Path, destination: Path) -> None:
prefix = sdist.name.removesuffix(".tar.gz") + "/"
with tarfile.open(sdist, "r:gz") as archive:
for member in archive.getmembers():
name = member.name.removeprefix(prefix)
if not member.isfile() or not (
name.startswith("tests/")
or name in _CONFIGS
or name == "tools/validate_runtime.py"
):
continue
target = (destination / name).resolve()
if not target.is_relative_to(destination):
message = f"Invalid consumer archive path: {member.name}"
raise ValueError(message)
stream = archive.extractfile(member)
if stream is None:
message = f"Cannot read consumer file: {member.name}"
raise ValueError(message)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(stream.read())


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("wheel", type=Path)
parser.add_argument("sdist", type=Path)
parser.add_argument("--python", required=True, help="Consumer Python version")
args = parser.parse_args()
uv = shutil.which("uv")
if uv is None:
parser.error("uv is required to create the isolated consumer environment")
wheel, sdist = args.wheel.resolve(), args.sdist.resolve()
evidence = check_distribution(wheel, sdist)
json.dump(evidence, sys.stdout, indent=2)
sys.stdout.write("\n")
sys.stdout.flush()
environment = dict(os.environ, UV_TORCH_BACKEND="cpu")
for key in ("PYTHONPATH", "PYTHONHOME", "TOX_ENV_NAME"):
environment.pop(key, None)

with tempfile.TemporaryDirectory(prefix="bearshape-installed-") as temporary:
consumer = Path(temporary).resolve()
_copy_consumers(sdist, consumer)
requirements = consumer / "requirements.txt"
subprocess.run(
[
uv,
"export",
"--locked",
"--no-default-groups",
"--group",
"optional",
"--group",
"static",
"--group",
"test",
"--no-emit-project",
"--output-file",
str(requirements),
],
check=True,
cwd=consumer,
env=environment,
stdout=subprocess.DEVNULL,
)
venv = consumer / ".venv"
subprocess.run(
[uv, "venv", "--python", args.python, str(venv)],
check=True,
cwd=consumer,
env=environment,
)
python = venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
subprocess.run(
[uv, "pip", "sync", "--python", str(python), str(requirements)],
check=True,
cwd=consumer,
env=environment,
)
subprocess.run(
[
uv,
"pip",
"install",
"--python",
str(python),
str(wheel),
"beartype==0.23.0rc0",
],
check=True,
cwd=consumer,
env=environment,
)
subprocess.run(
[str(python), "-I", "-c", _ORIGIN_CHECK],
check=True,
cwd=consumer,
env=environment,
)
subprocess.run(
[str(python), "-I", "tools/validate_runtime.py", "cpu"],
check=True,
cwd=consumer,
env=environment,
)
subprocess.run(
[str(python), "-I", "-m", "pytest", "tests/", "--installed-package", "-n", "4"],
check=True,
cwd=consumer,
env=environment,
)


if __name__ == "__main__":
main()
Loading