From e5d05bf6a54eb8ae462a1e88ceb4cc3ccaff8565 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Tue, 28 Apr 2026 15:10:47 -0400 Subject: [PATCH 1/3] DOC: Add rudimentary CLAUDE.md inspired by absent ai annotations in proposed PRs --- CLAUDE.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..ce3a99d6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,44 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repo Overview +- `dandischema` is a pure-Python library defining the DANDI metadata schemata (no CLI). +- Source lives under `dandischema/`, tests under `dandischema/tests/`. +- Helper scripts (schema publishing, license fetching, etc.) live under `tools/`. +- `pyproject.toml` is the single source of truth for dependencies and tool config; `tox.ini` orchestrates testing/linting; `.pre-commit-config.yaml` runs auto-formatters. + +## Build/Test Commands +- Run full test suite: `tox -e py3` (or `tox -e py` to use the active interpreter). +- Run a single test: `tox -e py3 -- dandischema/tests/test_models.py::test_function -v`. +- Inside an active venv (`uv venv` / `source .venv/bin/activate`): `python -m pytest dandischema`. +- Lint: `tox -e lint` (runs `codespell` + `flake8` over `dandischema/`). +- Type checking: `tox -e typing` (runs `mypy dandischema`). +- Lint + types together: `tox -e lint,typing`. +- Set `DANDI_TESTS_NONETWORK=1` to make tests refuse outbound HTTP (the `disable_http` autouse fixture in `dandischema/tests/conftest.py` points proxies at a dead address). +- DataCite-dependent tests require `DATACITE_DEV_LOGIN` / `DATACITE_DEV_PASSWORD`; set `NO_ET=1` to disable etelemetry. Both are passed through by tox. +- There is no `docker-compose` in this repo and no `hatch run test:run` env — don't suggest those (the dandi-cli pattern doesn't apply here). + +## Pre-commit +- Install hooks if missing (no `.git/hooks/pre-commit` present): `pre-commit install`. +- Hooks run: trailing-whitespace, end-of-file-fixer, check-yaml, check-added-large-files, black, isort, codespell, flake8. +- If a commit fails because hooks auto-fixed files, just re-run `git commit` — the second attempt usually succeeds. Investigate further only if it fails twice. + +## Test Markers +- Mark AI-generated tests with `@pytest.mark.ai_generated`. +- This project does **not** have a `pytest_plugin.py`. If a new marker is introduced, register it under a `[pytest]`/`markers =` block in `tox.ini` (or via `pytest.ini`/`conftest.py`) — otherwise `filterwarnings = error` (set in `tox.ini`) will turn the unregistered-marker warning into a failure. + +## Code Style +- Black-formatted, line length 100 (see `.flake8: max-line-length = 100`; flake8 ignores `E203,W503`). +- isort with `profile = "black"`, `force_sort_within_sections = true`, `reverse_relative = true`, `known_first_party = ["dandischema"]`. +- Type annotations required for all new code — `mypy` runs in strict-ish mode (`allow_untyped_defs = false`, `warn_return_any`, `warn_unreachable`, `pydantic.mypy` plugin enabled). Models use Pydantic v2 (`pydantic[email]~=2.4`). +- Class names: CamelCase; functions/variables: snake_case. +- Exception names end with `Error` (see `dandischema/exceptions.py`). +- Docstrings in NumPy style for public APIs; keep them in sync with signature changes. +- Prefer specific exceptions over generic ones. +- Imports organized stdlib / third-party / local, alphabetical within groups (isort handles ordering). +- All imports at module top — no function-local imports (per global rule), unless guarding an optional dep with `try/except ImportError`. + +## Schema / Data Files +- `dandischema/*.yaml` (e.g. `standards.yaml`, `model_standards.yaml`, `personinfo.yaml`) are tracked schema/data inputs — don't reformat or rename casually; downstream consumers and `tools/pubschemata.py` rely on them. +- `dandischema/_version.py` is generated by `versioningit` (see `[tool.versioningit.write]`); don't edit by hand. From 3cc8e7a2c22cf1d9041954ba09cd5865dc396923 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Tue, 28 Apr 2026 16:15:22 -0400 Subject: [PATCH 2/3] Clarify to keep docs in sync Co-authored-by: Isaac To --- CLAUDE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index ce3a99d6..044f85c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,3 +42,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Schema / Data Files - `dandischema/*.yaml` (e.g. `standards.yaml`, `model_standards.yaml`, `personinfo.yaml`) are tracked schema/data inputs — don't reformat or rename casually; downstream consumers and `tools/pubschemata.py` rely on them. - `dandischema/_version.py` is generated by `versioningit` (see `[tool.versioningit.write]`); don't edit by hand. + +## Documentation Freshness +- When preparing a PR, check whether the documentation — both human-facing (e.g. `README.md`, docstrings, comments) and machine-facing (e.g. this `CLAUDE.md`, schema descriptions) — is still accurate with respect to the changes the PR introduces. If anything has drifted, update it as part of the same PR. From f95ad2b0f1016f3dc2b8d4933261ec4f21b2cac6 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Tue, 28 Apr 2026 16:17:11 -0400 Subject: [PATCH 3/3] Add dandischema pytest plugin (deps header, DandiBaseModel diff, ai_generated marker) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapted from dandi-cli's `dandi/pytest_plugin.py`: - `pytest_report_header` lists versions of declared dependencies. - `pytest_assertrepr_compare` produces a useful dict-level diff for `DandiBaseModel == DandiBaseModel` failures instead of opaque pydantic reprs. - `pytest_configure` registers custom markers — currently only `ai_generated` (the only custom marker actually used here). Registered via `[project.entry-points.pytest11]` in pyproject.toml so it auto-loads after `pip install`. Tox: - Drop the hardcoded `dandischema` argument from the `pytest` invocation in `tox.ini` so that `tox -e py3 -- dandischema/tests/test_foo.py::test_bar` passes the path straight through to pytest. With no posargs, pytest still discovers tests from the current directory. - Set `--import-mode=importlib` in `[pytest]` addopts to avoid `ImportPathMismatchError`: with the plugin entry point, pytest now eagerly imports `dandischema` from the installed copy in the tox venv, which collides with collecting `dandischema/tests/conftest.py` from the source tree under the default "prepend" import mode. Updated CLAUDE.md to describe the new plugin under "Test Markers / Pytest Plugin", replacing the previous note that this repo had no plugin. Co-Authored-By: Claude Code 2.1.121 / Claude Opus 4.7 (1M context) --- CLAUDE.md | 8 ++++-- dandischema/pytest_plugin.py | 55 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 ++ tox.ini | 8 ++++-- 4 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 dandischema/pytest_plugin.py diff --git a/CLAUDE.md b/CLAUDE.md index 044f85c1..9b88c5a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,9 +24,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - Hooks run: trailing-whitespace, end-of-file-fixer, check-yaml, check-added-large-files, black, isort, codespell, flake8. - If a commit fails because hooks auto-fixed files, just re-run `git commit` — the second attempt usually succeeds. Investigate further only if it fails twice. -## Test Markers +## Test Markers / Pytest Plugin +- A pytest plugin lives at `dandischema/pytest_plugin.py` and is registered via `[project.entry-points.pytest11]` in `pyproject.toml` (so it's auto-loaded once the package is installed). +- It provides: + - `pytest_configure`: registers custom markers — currently only `ai_generated`. Add new markers to the `markers` list there (don't rely on `tox.ini`/`pytest.ini`); `filterwarnings = error` in `tox.ini` makes unregistered markers fail. + - `pytest_report_header`: prints versions of declared runtime dependencies on the pytest header. + - `pytest_assertrepr_compare`: custom diff for `DandiBaseModel == DandiBaseModel` so failures show the underlying dict diff instead of opaque pydantic reprs. - Mark AI-generated tests with `@pytest.mark.ai_generated`. -- This project does **not** have a `pytest_plugin.py`. If a new marker is introduced, register it under a `[pytest]`/`markers =` block in `tox.ini` (or via `pytest.ini`/`conftest.py`) — otherwise `filterwarnings = error` (set in `tox.ini`) will turn the unregistered-marker warning into a failure. ## Code Style - Black-formatted, line length 100 (see `.flake8: max-line-length = 100`; flake8 ignores `E203,W503`). diff --git a/dandischema/pytest_plugin.py b/dandischema/pytest_plugin.py new file mode 100644 index 00000000..8e928c2d --- /dev/null +++ b/dandischema/pytest_plugin.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, requires, version + +from packaging.requirements import Requirement +from pytest import Config + +from dandischema.models import DandiBaseModel + + +def pytest_configure(config: Config) -> None: + markers = [ + "ai_generated", + ] + for marker in markers: + config.addinivalue_line("markers", marker) + + +def pytest_report_header(config: Config) -> list[str]: + """Add version information for key dependencies to the pytest header.""" + try: + deps = {Requirement(dep).name for dep in (requires("dandischema") or [])} + except PackageNotFoundError: + deps = {"jsonschema", "pydantic", "requests"} + + versions = [] + for pkg in sorted(deps): + try: + version_str = f"-{version(pkg)}" + except PackageNotFoundError: + version_str = " NOT INSTALLED" + versions.append(f"{pkg}{version_str}") + + return [f"dependencies: {', '.join(versions)}"] if versions else [] + + +def pytest_assertrepr_compare(op: str, left: object, right: object) -> list[str] | None: + """Custom comparison representation for DandiBaseModel.""" + if ( + isinstance(left, DandiBaseModel) + and isinstance(right, DandiBaseModel) + and op == "==" + ): + ldict, rdict = dict(left), dict(right) + if ldict == rdict: + return [ + "dict representations of models are equal, but values aren't!", + f"Left: {left!r}", + f"Right: {right!r}", + ] + else: + # Rely on pytest "recursing" into interpreting the dict diff. + # TODO: could be further improved by accounting for ANY values etc. + assert ldict == rdict # for easier comprehension of diffs + return None diff --git a/pyproject.toml b/pyproject.toml index dca8b6b2..28889269 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,9 @@ dynamic = ["version"] Homepage = "http://dandiarchive.org" "Source Code" = "https://github.com/dandi/dandischema" +[project.entry-points.pytest11] +dandischema = "dandischema.pytest_plugin" + [project.optional-dependencies] style = [ "flake8", diff --git a/tox.ini b/tox.ini index 84f9d380..2a2eb841 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ passenv = DATACITE_DEV_PASSWORD NO_ET commands = - pytest -v {posargs} dandischema + pytest -v {posargs} [testenv:lint] skip_install = true @@ -31,7 +31,11 @@ commands = mypy dandischema [pytest] -addopts = --cov=dandischema --tb=short --durations=10 +# --import-mode=importlib avoids ImportPathMismatchError when the package +# (including dandischema/tests/) is also installed into the tox venv and the +# pytest plugin (registered via [project.entry-points.pytest11]) eagerly +# imports `dandischema` from site-packages. +addopts = --cov=dandischema --tb=short --durations=10 --import-mode=importlib filterwarnings = error #