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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,26 @@ jobs:
ETS_TOOLKIT: qt4
# empty for push/pull_request, so those runs keep the committed image
MAYAVI_RENDER_FLAKY: ${{ inputs.render_flaky }}
run: python scripts/render_docs.py
# under coverage because this is where sixty of the ninety examples are
# run -- the other thirty are `pytest examples` in tests.yml. Every
# example is a subprocess, so it reports anything at all only because of
# `patch = ["subprocess"]`; render_docs.py chdirs into docs/source, but
# the data files still land beside pyproject.toml, where combine wants
# them.
run: coverage run scripts/render_docs.py
- name: Combine coverage
if: '!cancelled()'
run: |
coverage combine
coverage xml
coverage report --show-missing --skip-covered
- uses: codecov/codecov-action@v7.0.0
if: '!cancelled()'
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
# informational only -- a failed upload must not fail the job
fail_ci_if_error: false
- name: Report regenerated files
# --stat alone shows nothing for a figure that is new rather than
# changed, which is how the gallery went years with images CI rendered
Expand Down
67 changes: 66 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,76 @@ jobs:
files: coverage.xml
fail_ci_if_error: false

# The examples the gallery does not render, which is where every other one
# is run: examples/tvtk, the explorer application, the four top-level ones,
# and the handful that neither show a figure nor open a dialog. Which those
# are is asked of docs/source/render_examples.py rather than listed, so the
# two sets cannot drift apart -- between them every example in the repository
# is executed on every PR.
#
# A job of its own for the reasons `integration` is one: they want a display
# and the [app] extra, and a step conditioned on the matrix stops running in
# silence when the matrix moves under it. One VTK is enough -- what these
# actually exercise is the ETS API the examples are written against, which is
# why the `ets: main` row is here and not only on the matrix above: every bug
# this suite turned up when it was written (a pyface 8 widget that no longer
# creates its own control, taking IVTK's splitter down with a null; a
# PipelineBrowser attribute the workbench view had wrong) was that kind.
examples:
name: Examples ${{ (matrix.ets && 'ets-main') || '' }}
strategy:
matrix:
ets: ['', 'main']
fail-fast: false
runs-on: ubuntu-latest
defaults:
run:
shell: bash
timeout-minutes: 20 # the suite takes about a minute
env:
ETS_TOOLKIT: qt4
QT_API: pyside6
PYTHONUNBUFFERED: '1'
steps:
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
fetch-depth: 0 # setuptools_scm needs the tags
- uses: pyvista/setup-headless-display-action@v4
with:
qt: true
- uses: actions/setup-python@v7.0.0
with:
python-version: '3.14'
- run: python -m pip install --upgrade pip # --group needs pip >= 25.1
# scipy is the only third-party import in this set (array_animation.py);
# the rendered examples want more, and docs.yml's `docs` group has those
- run: python -m pip install --group test pyside6 scipy -ve ".[app]"
- uses: ./.github/actions/install-ets-main
if: matrix.ets == 'main'
- run: python -c "import vtk; print(f'VTK {vtk.VTK_VERSION}')"
# every case is a subprocess, so this reports anything at all only because
# of `patch = ["subprocess"]`; the wrapper's own per-example timeout is
# tighter than pytest's, which is the backstop behind it
- run: coverage run -m pytest -v --timeout=180 examples
- name: Combine coverage
if: '!cancelled()'
run: |
coverage combine
coverage xml
coverage report --show-missing --skip-covered
- uses: codecov/codecov-action@v7.0.0
if: '!cancelled()'
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
fail_ci_if_error: false

# Scheduled runs have no associated PR where a failure would be noticed, so
# open an issue (if there isn't one already) when they break
issue-on-failure:
name: Open issue on scheduled failure
needs: [tests, integration]
needs: [tests, integration, examples]
if: failure() && github.event_name == 'schedule'
runs-on: ubuntu-latest
permissions:
Expand Down
21 changes: 20 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ python -m build
# Test suites (CI runs exactly these)
pytest -v --timeout=10 mayavi
pytest -sv --timeout=60 tvtk
pytest -v --timeout=180 examples
```

- The files in `integrationtests/mayavi/` are not pytest modules — each is an `optparse` script subclassing `TestCase(Mayavi)`, meant to be run as `python test_contour.py`, and importing one hands pytest `Test*` classes it cannot instantiate.
Expand All @@ -45,6 +46,13 @@ pytest -sv --timeout=60 tvtk
A *step* on a `tests` row was tried first and never ran: `!matrix.vtk` matched nothing because the `vtk-dev` include overrides no original matrix value, so GitHub merges it into the base ubuntu combination instead of adding a row, and every ubuntu row therefore has `vtk` set.
CI was green throughout.
A matrix-conditioned step fails silently that way; a job's absence from the checks is visible.
- `pytest examples` runs the examples the gallery does not, one subprocess each (30 of the 90, ~1 min), in `tests.yml`'s own `examples` job.
`mayavi/tests/common.py:run_example_headless` is what the child calls: it stubs out everything an example ends by blocking in — `mlab.show`, `GUI.start_event_loop`, `configure_traits`, `vtkRenderWindowInteractor::Start`, `QApplication.exec` — pushes the same re-raising traits handler the suites use, makes warnings fatal, and runs the rest of the script.
The filters are `EXAMPLE_WARNING_FILTERS` in the same file, shared with `scripts/render_docs.py` so that the two halves of the example set hold examples to one standard; pytest's own `filterwarnings` cannot do it, as it applies to the process pytest runs in and every case here is a subprocess.
Which examples those are is asked of `render_examples.rendered_examples()` rather than listed, so the two sets cannot drift: an example that stops being rendered starts being run here instead.
`user_mayavi.py` and `zzz_reader.py` are the exception (`RUN_AS_MODULE`) — both `sys.exit(1)` when run as `__main__`, being meant for the application to import, so they are run under their own module name for their module body.
`examples/conftest.py` keeps pytest from importing the example scripts themselves, as `integrationtests/conftest.py` does.
Both `ets` rows run, unlike `integration`'s reasoning about envisage: what these exercise is the ETS API the examples are written against, and every bug the suite found when it was written was ETS drift (pyface 8 widgets that no longer create their own control, which took `IVTK`'s `QSplitter` down with a null; `browser_view.py` reaching for a `PipelineBrowser.ui` that is spelled `_ui`).
- Regeneration is skipped if `tvtk/tvtk_classes.zip` is < 120 s old (`_tvtk_built_recently` in `setup.py`).
- Warnings are errors.
The filters live in `mayavi/tests/conftest.py` and `tvtk/tests/conftest.py` rather than `pyproject.toml`, so that they ship in the wheel and so reach the `pytest --pyargs` runs below and in `wheel.yml`.
Expand Down Expand Up @@ -78,6 +86,11 @@ If a warning is genuinely unfixable, add it to `nitpick_ignore` in `docs/source/
The toolkit is a per-process choice, so `capture_in_subprocess` sets `ETS_TOOLKIT=wx` in the child for any example whose source imports `wx` (`is_wx_example`), and `capture_one` sends it to `capture_wx_dialog` — the wx counterpart of `capture_dialog`, `WindowDC`/`MemoryDC` in place of `QWidget.grab`.
Note it cannot use `keep_windows_in_background()`, which is Qt-only, so a local render of those two will take focus.
wxPython has no Linux wheels on PyPI: `docs.yml` takes them from `extras.wxpython.org`, which is published per Ubuntu release and per Python — currently cp313 at the newest, which is why that job pins Python 3.13 and `ubuntu-24.04` rather than `-latest`.
- `capture_one` pushes a re-raising traits exception handler, as the suites' conftests do.
Without it an exception inside a notification handler is printed and swallowed, the example renders a figure with whatever that handler was going to draw missing from it, and the child exits 0 — which throws its output away.
That is how `coil_design_application` published a picture of two coils and no magnetic field: `np.NAN` went away in NumPy 2 and the `_get_Bnorm` property that computes the field raised on every call, silently (gh-1418).
- `render_examples.rendered_examples()` is the list of examples the gallery runs, and `examples/test_examples.py` runs the complement, so between the two every example is executed on every PR.
`EXAMPLE_DIR` is absolute for that reason — the helper has to answer the same thing from outside `docs/source`.
- Parts of `docs/source/mayavi/auto/` are generated: `mlab_reference.py` (repo root) emits the mlab API reference, `docs/source/render_examples.py` emits the example gallery.
Both are re-run in CI and both are also committed, so a plain `make -C docs html` works offline — which means a generator change must be committed **together with** its regenerated output, or `-W` fails on the stale copies.
- Regenerate with `python scripts/render_docs.py`, which drives all of them in the right order.
Expand All @@ -102,7 +115,8 @@ If a warning is genuinely unfixable, add it to `nitpick_ignore` in `docs/source/
They were `enthought_mayavi_mlab_*` until 2026-07, from the pre-2010 `enthought.mayavi` package name — which meant `mlab_reference.py` looked for names that did not exist and the mlab reference shipped with no illustrations at all for years.
- A rebuild is byte-for-byte reproducible, so regenerating shows a diff only where something really changed: the doc version is truncated to `4.8.4.dev` (the commit and date would retitle every page), `html_last_updated_fmt` is off with the build date carried by the site landing page alone, and the renderers seed `np.random` because several `mlab.test_*` functions plot random data.
`FLAKY_EXAMPLES` in `render_examples.py` names the examples that still are not reproducible: `tvtk_in_mayavi` and `magnetic_field`, which draw overlapping translucent actors that VTK composites differently in ~1% of pixels (roughly one run in five, and three of four, respectively), and `wx_mayavi_embed_in_notebook`, a screenshot of a wx window whose notebook lands differently — it came back changed in two of the four CI runs after it was added, on the committed bytes both times.
Their committed images are reused rather than re-rendered, so the published figures stop flipping back and forth; set `MAYAVI_RENDER_FLAKY=1` (or tick `render_flaky` on a `workflow_dispatch`) to redo them deliberately.
They are still *run* — that is the only place they ever run — but their figure goes to a scratch directory and the committed image stays put, so the published figures stop flipping back and forth; set `MAYAVI_RENDER_FLAKY=1` (or tick `render_flaky` on a `workflow_dispatch`) to redo them deliberately.
Skipping the run as well is what let `magnetic_field` sit broken: `np.arctan(x/y)` divides by zero on the axis of the coil, which is fatal under the render's warnings-as-errors, and nothing had executed it since the warnings became fatal.
For the first two, enabling depth peeling (it does engage — `last_rendering_used_depth_peeling` is 1) and forcing a `scene.render()` before the capture were both measured over ten runs and neither helps, so leave it alone rather than re-testing.
Beware that five runs is not enough to call this stable; that sample size gave a false positive twice.
- `mlab.savefig` honours the display's device pixel ratio, and neither `magnification=1` nor an explicit `size` overrides it, so a HiDPI display would give images 2x the size of CI's.
Expand Down Expand Up @@ -141,6 +155,8 @@ If the change touched a VTK workaround in any form — a `vtk_*_version` or `sys
Hence the `pip uninstall` first, and `assert_from_main.py` after, which reads each distribution's `direct_url.json`: nothing else distinguishes a real `main` install from a fall back to PyPI, and the row would be green either way.
The package list lives only in `requirements.txt` — `pip uninstall` takes it with `-r` and the script parses it — so the three uses cannot drift.
- It runs *after* the package install so nothing can undo it, which is why `pip check` stands in for the floor checking that installing first would have got from the resolver.
- The `examples` job runs `pytest examples` — the thirty examples the gallery does not render — under coverage, on both `ets` rows.
See **Building and testing locally** above for what it covers and why the ETS matrix is there.
- `.github/actions/open-issue` — composite action behind both `issue-on-failure` jobs: files an issue unless one with the same title is already open, appending the run URL.
Callers need an `actions/checkout` (sparse is enough) because a local action has to be on disk to be used, and `permissions: issues: write`.
- `wheel.yml` — mismatch matrix: build per-OS wheels against latest VTK, test them against all supported older VTKs (rows deliberately mirror `tests.yml` so failures are attributable to the mismatch), `twine check --strict`, and trusted publishing to PyPI on GitHub releases (environment `pypi`, `needs: [build, test, check]`).
Expand All @@ -150,6 +166,9 @@ If the change touched a VTK workaround in any form — a `vtk_*_version` or `sys
Doc build requirements live in the `docs` dependency group (PEP 735, `pip --group`), so the whole install is one `pip install --group docs -ve ".[app]"`.
Unlike `tests.yml` this build is *not* `--no-build-isolation`: nothing here pins an older VTK, so letting the isolated build fetch the latest is fine.
To review a PR's rendered docs, download the `docs-site` artifact and serve it (`python -m http.server`); GitHub has no linked HTML preview, and `deploy-pages`' `preview` input is alpha-gated.
The render step runs under `coverage run` and uploads to codecov like the other jobs: it is where sixty of the ninety examples execute, and every one of them is a subprocess, so it measures anything at all only because of `patch = ["subprocess"]`.
`render_docs.py` chdirs into `docs/source`, but coverage anchors its data files to the config it was started from, so they still land beside `pyproject.toml` where `coverage combine` wants them.
The generator itself stays unmeasured here — this build is isolated, and `tests.yml`'s install step is the one that covers it.
See **Documentation** below.

## Gotchas
Expand Down
8 changes: 4 additions & 4 deletions docs/source/mayavi/auto/coil_design_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,10 @@ def _get_Bnorm(self):
# to use an ImageData
Bmax = 10 * np.median(Bnorm)

Bx[Bnorm > Bmax] = np.NAN
By[Bnorm > Bmax] = np.NAN
Bz[Bnorm > Bmax] = np.NAN
Bnorm[Bnorm > Bmax] = np.NAN
Bx[Bnorm > Bmax] = np.nan
By[Bnorm > Bmax] = np.nan
Bz[Bnorm > Bmax] = np.nan
Bnorm[Bnorm > Bmax] = np.nan

self.Bx = Bx
self.By = By
Expand Down
13 changes: 7 additions & 6 deletions docs/source/mayavi/auto/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,19 +116,20 @@ def unstructured_grid():
cells = array([4, 0, 1, 2, 3, # tetra
8, 4, 5, 6, 7, 8, 9, 10, 11 # hex
])
# The offsets for the cells, i.e. the indices where the cells
# start.
offset = array([0, 5])
tetra_type = tvtk.Tetra().cell_type # VTK_TETRA == 10
hex_type = tvtk.Hexahedron().cell_type # VTK_HEXAHEDRON == 12
cell_types = array([tetra_type, hex_type])
# Create the array of cells unambiguously.
# Create the array of cells unambiguously. The cell array keeps the
# offsets itself, so there is no separate list of them to build: VTK 9.6
# deprecated both the CellArray.set_cells that took a count and the
# UnstructuredGrid.set_cells that took cell locations, and 9.7 removed the
# first of them outright.
cell_array = tvtk.CellArray()
cell_array.set_cells(2, cells)
cell_array.import_legacy_format(cells)
# Now create the UG.
ug = tvtk.UnstructuredGrid(points=points)
# Now just set the cell types and reuse the ug locations and cells.
ug.set_cells(cell_types, offset, cell_array)
ug.set_cells(cell_types, cell_array)
scalars = random.random(points.shape[0])
ug.point_data.scalars = scalars
ug.point_data.scalars.name = 'scalars'
Expand Down
29 changes: 16 additions & 13 deletions docs/source/mayavi/auto/magnetic_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,19 +108,22 @@ def magnetic_field(r, n, r0, R):
y = r[:, 1]
z = r[:, 2]
rho = np.sqrt(x**2 + y**2)
theta = np.arctan(x/y)
theta[y==0] = 0

E = special.ellipe((4 * R * rho)/( (R + rho)**2 + z**2))
K = special.ellipk((4 * R * rho)/( (R + rho)**2 + z**2))
Bz = 1/np.sqrt((R + rho)**2 + z**2) * (
K
+ E * (R**2 - rho**2 - z**2)/((R - rho)**2 + z**2)
)
Brho = z/(rho*np.sqrt((R + rho)**2 + z**2)) * (
-K
+ E * (R**2 + rho**2 + z**2)/((R - rho)**2 + z**2)
)
# on the axis of the coil rho and y are zero, so several of these divide by
# zero; what that produces is replaced just below
with np.errstate(divide='ignore', invalid='ignore'):
theta = np.arctan(x/y)
theta[y==0] = 0

E = special.ellipe((4 * R * rho)/( (R + rho)**2 + z**2))
K = special.ellipk((4 * R * rho)/( (R + rho)**2 + z**2))
Bz = 1/np.sqrt((R + rho)**2 + z**2) * (
K
+ E * (R**2 - rho**2 - z**2)/((R - rho)**2 + z**2)
)
Brho = z/(rho*np.sqrt((R + rho)**2 + z**2)) * (
-K
+ E * (R**2 + rho**2 + z**2)/((R - rho)**2 + z**2)
)
# On the axis of the coil we get a divided by zero here. This returns a
# NaN, where the field is actually zero :
Brho[np.isnan(Brho)] = 0
Expand Down
4 changes: 2 additions & 2 deletions docs/source/mayavi/auto/mlab_visual.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@

# Even sillier animation.
b1 = visual.box()
b2 = visual.box(x=4., color=visual.color.red)
b3 = visual.box(x=-4, color=visual.color.red)
b2 = visual.box(x=4., color=(1, 0, 0))
b3 = visual.box(x=-4, color=(1, 0, 0))
b1.v = 5.0

@mlab.show
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_compute_in_thread.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_contour.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_contour_contour.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_delaunay_graph.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_glyph.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_lorenz.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_lorenz_ui.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_mayavi_traits_ui.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_multi_block.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_multiple_engines.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_numeric_source.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_poll_file.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_polydata.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_qt_embedding.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_scatter_plot.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_streamline.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_structured_grid.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_structured_points2d.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_structured_points3d.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_superquad_with_gui.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_surf_regular_mlab.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/example_unstructured_grid.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/mayavi/generated_images/mayavi_mlab_fancy_mesh.jpg
Loading