- name
- pyvista-dev
- description
- Develop for PyVista the way the project works. Load before writing code for this repository.
# Developing for PyVista
Two documents already carry most of the rules, and this skill does not repeat them.
Read them, then come back here.
- `CONTRIBUTING.rst` is normative. Sections you will need:
| Topic | Section |
| --------------------------------------- | ---------------------------------------------------------- |
| Coding style, keyword-only arguments | `Coding Style` |
| Standard library import rules | `Import Conventions` |
| numpydoc rules and the sample docstring | `Docstrings` |
| Three-step deprecation policy | `Deprecating Features or other Backwards-Breaking Changes` |
| Image regression testing | `Notes Regarding Image Regression Testing` |
| Branch prefixes | `Branch Naming Conventions` |
| `make` targets and tox environments | `Quick Development Commands` |
- `context7.json` at the repository root holds the project's agent-facing API rules:
wrap rather than subclass, prefer PyVista over raw VTK, filters return new datasets,
the image-regression discipline. It is schema-validated by `pre-commit`. Treat it as the source for how to _use_ the API; this skill covers how to
_change_ it.
## The workflow
Build here, wrap with **pyvista-vtk**, test with **pyvista-testing**, critique with
**pyvista-review**, ship with **pyvista-pr**. `AGENTS.md` at the repository root routes to
all five and carries the rules that apply before any of them.
Run the critique stage in a **subagent** and fix what it finds before opening anything.
## What the project is for
PyVista is a Pythonic interface to VTK. A feature that makes users think in VTK terms
has failed even when it works.
`CONTRIBUTING.rst` names one value that is easy to skim past and that often decides
reviews: the project wants "good code, concise accurate documentation, and avoiding
unneeded code churn". Churn is called out by name. Prefer the smaller diff.
## Before writing code
Two assumptions cause most of the rework here, and both are one `grep` from certainty:
- **that the name is free.** `cell_connectivity` exists because `connectivity` collides
with `DataSetFilters.connectivity`. Search the class, its bases, and the filter mixins
before choosing a public name.
- **that the constant does not already exist.** Version checks, capability probes,
`Literal` aliases and test guards are the single most requested reuse in review.
Version-dependent behavior in particular must use the shared constant rather than a
locally re-derived comparison, because the local copy is what goes stale.
Where a design question has two defensible answers that lead to different code, ask
rather than picking one silently and building on it.
## Comments and docstrings
Less is more. A comment earns its place by explaining why, not what; if the code already
says what, delete the comment. Keep docstrings to what the numpydoc sections require, not
a narrative.
Every function and class gets a docstring, including small private helpers -- a one-line
summary is enough when there is nothing more to say. Only the full numpydoc sections
(`Parameters`, `Returns`, examples, ...) are reserved for the public API.
`numpydoc ignore=RT01` (no `Returns` section) is fine for a simple property getter or
setter, where the summary line already says what comes back. A function whose return
type or meaning is not obvious from the name and summary needs a real `Returns` section
instead of the ignore, however small the function is.
Never describe how the change came to be -- no "fix", "bug", "temporary workaround", "now
we", "previously", or reference to a prior version of the code. Write both as if the
current diff were the first and only version of the file: they describe the code that
exists on this branch relative to `main`, not the debugging path taken to get there.
## Size
Merged pull requests here are small: the median adds tens of lines, not hundreds. One
self-contained change per pull request, and refactors go in a separate one from features
and bug fixes. If you find an unrelated bug while working, note it and open a second pull
request rather than folding the fix in.
Large changes are sometimes correct, and a few percent of merged pull requests are large.
The recurring legitimate shapes are a cohesive new subsystem whose halves do not stand
alone, a mechanical sweep that must land atomically or the tree is inconsistent, a
deprecation that has to move every internal call site at once, and a feature whose tests
dwarf it. A change split into pieces that cannot be reviewed or merged independently is
worse than one large change.
When a change has to be large, spend the effort on the reviewer:
- Say in one line why it must land atomically.
- Keep mechanical commits separate from semantic ones. A wide rename plus a small
behavior change is reviewable as two commits and unreadable as one.
- Name the two or three files carrying the real change and say the rest is mechanical.
- Never fold an opportunistic refactor into an already-large change.
Regenerate the size distribution with:
```bash
gh pr list -R pyvista/pyvista --state merged --limit 400 \
--json additions,author --jq '.[] | select(.author.is_bot | not) | .additions'
```
Exclude bots, as above. Dependency bumps are a seventh of merged pull requests and pull
every percentile down.
## Conventions that are machine-enforced
`ruff` and `numpydoc` settle most style questions, so do not spend review budget on
them. The custom `pre-commit` hooks are easier to trip, and they produce a red job rather
than a comment:
| Hook | Rejects | Use instead |
| ------------------------------ | --------------------------------------------------------------- | ------------------------------------------ |
| `no-bare-import-pyvista` | a bare `import pyvista` | `import pyvista as pv` |
| `no-forbidden-plotter-names` | `plotter`, `p`, `plot`, `plt`, `pltr` as the `Plotter` variable | `pl` |
| `namespace-stdlib-imports` | `import pathlib` and friends | `from pathlib import Path` |
| `no-lint-suppression-comments` | `noqa` and `ruff:` directives under `examples/` | fix it, or `per-file-ignores` in pyproject |
| `no-import-error-skip` | `importorskip`, `except ImportError`, `suppress(ImportError)` | make the dependency available |
| `warn_external` | `warnings.warn` inside `pyvista/` | `warn_external` |
`warn_external` is the one that behaves differently: it is a `libcst` codemod, so it
rewrites the call for you and fails the run because the file changed. Stage its rewrite
rather than reverting it.
Scope matters when a snippet fails one of these: `namespace-stdlib-imports` and
`no-forbidden-plotter-names` run on Python, reStructuredText and Markdown;
`no-bare-import-pyvista` runs on reStructuredText and Markdown only;
`no-lint-suppression-comments` runs under `examples/` and `no-import-error-skip` under
`tests/`. A documentation snippet can fail a hook that the equivalent Python file would
pass.
Two more that `ruff` enforces and reviewers still notice: error messages are assigned to
a variable before being raised (`EM`), and boolean arguments are keyword-only
(`FBT001`/`FBT002`). Making an existing signature keyword-only goes through
`_deprecate_positional_args`, never a hard break. The counterpart of
`namespace-stdlib-imports` is ruff's `banned-from` list (`ICN003`) in `pyproject.toml`,
which forbids the opposite direction; `Import Conventions` explains why both exist.
## Editing TOML
`taplo-format` runs `reorder_keys=true` and `reorder_arrays=true`, so keys and array
entries in `pyproject.toml` and other TOML files get sorted. A comment on its own line
above an entry is attached to that entry and breaks the sort -- `taplo` will not reorder
past it. Put a comment inline, on the same line as the value it documents, so sorting
still works. Keep it short for the same reason as everywhere else: less is more.
A comment line is legitimate only when it deliberately splits the array or table into
independently-sorted sections -- `filterwarnings` in `pyproject.toml` keeps `'error'`
first with a blank line rather than a comment because pytest applies filters in order, and
the `pytest-pyvista options` comment splits the `pytest-pyvista` keys from pytest's own so
each group sorts on its own. Reach for that only when order or grouping is load-bearing,
not to leave a note.
## Tests
The heaviest review axis by a wide margin. `CONTRIBUTING.rst` and `context7.json` cover
what to write. This is what gets sent back:
- **Prove the test fails without the fix.** Revert the change, watch it go red, restore.
It is the only reliable way to catch a test that passes for an unrelated reason.
- **Watch for incidental passes.** A uniform mesh takes a different code path than a
mixed one, and a fixture can make an assertion true regardless of the change.
- **Test the negative case.** If a feature guards against something, make that something
happen in a test.
- **Assert enough.** Round-trip equality rather than a single attribute.
- Parametrize rather than branching inside the test body, and do not leave a case
commented out in a `parametrize` list.
- `filterwarnings` in `pyproject.toml` begins with `error`, so any warning that is not
explicitly ignored fails the suite.
## Local gates
`Quick Development Commands` lists the `make` targets, each mirroring a CI job. Run
`make lint`, `make docstyle`, `make doctest`, and the test module you touched, and treat a
change as unfinished until they pass.
`make doctest` is the one that gets skipped for looking unrelated: it runs every docstring
example in the package rather than the ones in the diff, so anything that changes
import-time behavior or a plotting default can fail it without a docstring edit anywhere.
Moving `pv.BUILDING_GALLERY` out of `pyvista/ext/plot_directive.py`'s module scope did
exactly that -- collecting that module was what set the flag, and one example's
anti-aliasing warning is silenced only while a gallery is being built.
The target runs two environments: `doctest-modules` executes the examples, and
`doctest-names` statically checks that every name an example uses is bound by that example
(`tests/check_doctest_names.py`). The second executes none of the examples, so its
failures are name resolution bugs in the docstring rather than runtime errors.
`tests/conftest.py` and the `doctest-modules` tox environment already set off-screen
rendering, so the `make` targets are safe. Only a bare `pytest --doctest-modules` outside
tox needs `PYVISTA_OFF_SCREEN=true`; without it the examples open render windows and take
over the display.
**Never push a commit to find out whether something passes.** `AGENTS.md` opens with that
rule and `CONTRIBUTING.rst` states it as `Continuous Integration Etiquette`. It binds you
more tightly than a human contributor, because pushing is cheaper for you than it is for
the project. Keep the pull request in draft while you iterate.
## Approaching a red job
`Continuous Integration Etiquette` covers the basics: read the log and find the assertion,
reproduce locally, and diagnose a flaky test rather than re-running the job. Beyond that:
- Check whether the failure also occurs on `main` before attributing it to the branch. A
second worktree at `origin/main` settles this in a minute.
- A version-specific failure needs the shared version constant, not a local workaround.
- An image regression failure is settled from the job's `failed_test_images-*` artifact,
not by pushing a regenerated baseline to see whether it sticks. See **pyvista-testing**.
عرض على GitHub