Skip to content

fix(viewer): measure the 3d scene fill from mplot3d and add CI on Python 3.8-3.14 - #8

Merged
EternalTime merged 4 commits into
masterfrom
fm/ci-pyleafs
Aug 19, 2026
Merged

fix(viewer): measure the 3d scene fill from mplot3d and add CI on Python 3.8-3.14#8
EternalTime merged 4 commits into
masterfrom
fm/ci-pyleafs

Conversation

@EternalTime

Copy link
Copy Markdown
Owner

Intent

Make GitHub run pyLEAFS's tests automatically on every push and every pull request, so nothing lands untested. Add .github/workflows/tests.yml that installs the library exactly the way the repository's published instructions describe (pip install -e '.[test]', using the declared test extra rather than installing pytest by hand), runs pytest, and does so across the full range of Python versions the repository CLAIMS to support - 3.8 through 3.14, read from requires-python, the classifiers, and the README - rather than a convenient subset. A second job builds the manual with the docs extra (pip install -e '.[docs]'; make -C docs html) so a broken documentation build is caught too. The workflow is deliberately boring: only actions/checkout and actions/setup-python, a plain version matrix with fail-fast disabled, and no caching cleverness or third-party actions.

While verifying the matrix, Python 3.8 turned out to genuinely fail: 3.8 caps matplotlib at 3.7.5, and test_viewer_3d_fill_is_the_true_worst_case_over_all_view_angles asserted against hard-coded fill constants (0.9526, 1.0302) that only hold on matplotlib 3.8+. matplotlib 3.8 multiplied its normalised 3d box aspect by an extra 25/24, so the same world box covers more of a 3d axes there than on 3.7; under the default perspective projection that enlargement is not a plain rescale, so both the width and the height fractions grow, and by slightly different amounts (3.7.5 measures 0.91276, 0.98890). The captain's explicit decision was to KEEP Python 3.8 supported and fix the cause rather than drop the version or loosen the test, and authorised expanding scope for exactly that. So pyLEAFS/viewer.py now derives those two numbers instead of hard-coding them: _scene_fill_3d() measures mplot3d's own projection of a unit box at the two closed-form worst view angles on a throwaway off-screen figure and caches the result, which is correct on every matplotlib version. The value is a property of mplot3d alone - verified independent of world shape, figure size, and axes rectangle - so measuring it once off-screen is legitimate. The test now compares the swept maximum against that measurement instead of a literal, and because the sweep repeats the identical worst-case projection it lands on the measured value to within pixel-transform rounding, so the no-overflow assertion carries a 1e-9 epsilon while the 5e-4 'nothing is lost' tolerance is unchanged; the test was not weakened into meaninglessness. Verified locally: the full suite (38 tests) passes on 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 and 3.14, including a cross-check with matplotlib 3.7.5 pinned on 3.9, and the docs build succeeds. The published install instructions themselves proved correct on a clean environment, so no instruction fix was needed. Scope is deliberately limited to the workflow plus the viewer/test fix the 3.8 decision forced.

What Changed

  • pyLEAFS/viewer.py now derives the 3d scene fill instead of hard-coding it: _scene_fill_3d() measures mplot3d's own projection of a unit box at the two closed-form worst view angles on a throwaway off-screen figure, cached with functools.lru_cache. The old _SCENE_FILL_3D = (0.9526, 1.0302) constants only held on matplotlib 3.8+, which scaled the normalised 3d box aspect by an extra 25/24, so the 3d viewer window is now sized correctly on matplotlib 3.7 as well - the version Python 3.8 is capped at.
  • tests/test_pyLEAFS.py compares the swept maximum against that measurement rather than the literals; the no-overflow assertion carries a 1e-9 epsilon because the sweep repeats the identical worst-case projection, while the 5e-4 "nothing is lost" tolerance is unchanged.
  • New .github/workflows/tests.yml runs pip install -e '.[test]' then pytest on every push and pull request across Python 3.8-3.14 (fail-fast: false, pinned to ubuntu-24.04 since no 3.8/3.9 builds exist for newer images), plus a second job that builds the manual with pip install -e '.[docs]' and make -C docs html. Only actions/checkout and actions/setup-python, no caching, permissions: contents: read. docs/Makefile defaults SPHINXOPTS to -W so Sphinx warnings fail that build.

Risk Assessment

✅ Low: All four previously agreed fixes were applied exactly as instructed and verified against the runner-image and python-versions manifests, the Sphinx config, and the test import, leaving a well-bounded change that satisfies every stated acceptance criterion with no outstanding findings.

Testing

I ran the new workflow's two jobs locally, step for step, on fresh checkouts: pip install -e '.[test]' + pytest passes 38 tests on all seven Python versions the repository claims (3.8 through 3.14, with matplotlib resolving to 3.7.5 on 3.8), plus a matplotlib-3.7.5-pinned cross-check on 3.9, and the docs job builds the manual with -W proven active by making a toctree warning fail the build. I confirmed the Python 3.8 fix addresses a real failure rather than a hypothetical one - the base commit fails the fill test on matplotlib 3.7.5 with exactly the reported values while the target commit passes - and captured before/after screenshots of the 3d viewer showing the world box going from 95.2% x 96.0% of its reserved window area to 99.2% x 100.0% on 3.7.5, with modern matplotlib unaffected. I also verified the workflow structure against requires-python, the classifiers and the README, checked that the ubuntu-24.04 pin is what makes 3.8/3.9 resolvable at all, confirmed the measured fill is configuration-independent to 1e-16, and mutation-tested the assertions to show the 1e-9 epsilon did not weaken them. Everything passed; the worktree is clean and temporary environments were removed.

  • Evidence: 3d viewer on Python 3.8 / matplotlib 3.7.5, before vs after (worst-case view angle) (local file: /var/folders/ym/vnmfjh5n7dn2vdzdgy7zrbjh0000gn/T/no-mistakes-evidence/01M0D5TVKGS5R5XC1DPXK32H2J/viewer-mpl375-before-after.png)
  • Evidence: After: world box fills 99.2% x 100.0% of the reserved area on matplotlib 3.7.5 (local file: /var/folders/ym/vnmfjh5n7dn2vdzdgy7zrbjh0000gn/T/no-mistakes-evidence/01M0D5TVKGS5R5XC1DPXK32H2J/viewer-mpl375-after-annotated.png)
  • Evidence: Before: world box only fills 95.2% x 96.0% on matplotlib 3.7.5 (local file: /var/folders/ym/vnmfjh5n7dn2vdzdgy7zrbjh0000gn/T/no-mistakes-evidence/01M0D5TVKGS5R5XC1DPXK32H2J/viewer-mpl375-before-annotated.png)
  • Evidence: After on matplotlib 3.11.1: measured fill reproduces the old constants, no change for current users (local file: /var/folders/ym/vnmfjh5n7dn2vdzdgy7zrbjh0000gn/T/no-mistakes-evidence/01M0D5TVKGS5R5XC1DPXK32H2J/viewer-mpl311-after-annotated.png)
  • Evidence: Unannotated viewer frame as the end user sees it (matplotlib 3.7.5, after) (local file: /var/folders/ym/vnmfjh5n7dn2vdzdgy7zrbjh0000gn/T/no-mistakes-evidence/01M0D5TVKGS5R5XC1DPXK32H2J/viewer-mpl375-after-plain.png)
Evidence: Local transcript of both workflow jobs across the full matrix

----- tests (python-version: 3.8) ----- python 3.8.20 | matplotlib 3.7.5 | numpy 1.24.4 | pytest 8.3.5 38 passed in 39.23s ----- tests (python-version: 3.9) ----- python 3.9.25 | matplotlib 3.9.4 | numpy 2.0.2 | pytest 8.4.2 38 passed, 14 warnings in 46.01s ----- tests (python-version: 3.10) ----- python 3.10.21 | matplotlib 3.10.9 | numpy 2.2.6 | pytest 9.1.1 38 passed in 46.05s ----- tests (python-version: 3.11) ----- python 3.11.16 | matplotlib 3.11.1 | numpy 2.4.6 | pytest 9.1.1 38 passed in 44.93s ----- tests (python-version: 3.12) ----- python 3.12.14 | matplotlib 3.11.1 | numpy 2.5.2 | pytest 9.1.1 38 passed in 46.02s ----- tests (python-version: 3.13) ----- python 3.13.15 | matplotlib 3.11.1 | numpy 2.5.2 | pytest 9.1.1 38 passed in 46.42s ----- tests (python-version: 3.14) ----- python 3.14.7 | matplotlib 3.11.1 | numpy 2.5.2 | pytest 9.1.1 38 passed in 45.57s ----- cross-check: python 3.9 with matplotlib pinned to 3.7.5 ----- 38 passed, 11 warnings in 31.03s ----- docs job (3.14, pip install -e '.[docs]'; make -C docs html) ----- Running Sphinx v9.1.0 build succeeded. ----- docs: -W is in effect and does fail a warning ----- $ make -C docs -n html -> sphinx-build -M html "." "_build" -W with a page left out of the toctree: build finished with problems, 1 warning (with warnings treated as errors). make: *** [html] Error 1 ----- regression check: base commit 5c9d512 on python 3.8 / matplotlib 3.7.5 ----- FAILED test_viewer_3d_fill_is_the_true_worst_case_over_all_view_angles np.allclose(array([0.9127639, 0.9888995]), (0.9526, 1.0302), atol=0.0005) -> False same test at f2bf5a4 on the same interpreter: passed (fill measured as 0.91276 x 0.98890)

Local mirror of .github/workflows/tests.yml @ f2bf5a4 (fresh checkout per version,
same steps as the workflow: pip install -e '.[test]' then pytest)

----- tests (python-version: 3.8) -----
python 3.8.20 | matplotlib 3.7.5 | numpy 1.24.4 | pytest 8.3.5
38 passed in 39.23s
----- tests (python-version: 3.9) -----
python 3.9.25 | matplotlib 3.9.4 | numpy 2.0.2 | pytest 8.4.2
38 passed, 14 warnings in 46.01s
----- tests (python-version: 3.10) -----
python 3.10.21 | matplotlib 3.10.9 | numpy 2.2.6 | pytest 9.1.1
38 passed in 46.05s
----- tests (python-version: 3.11) -----
python 3.11.16 | matplotlib 3.11.1 | numpy 2.4.6 | pytest 9.1.1
38 passed in 44.93s
----- tests (python-version: 3.12) -----
python 3.12.14 | matplotlib 3.11.1 | numpy 2.5.2 | pytest 9.1.1
38 passed in 46.02s
----- tests (python-version: 3.13) -----
python 3.13.15 | matplotlib 3.11.1 | numpy 2.5.2 | pytest 9.1.1
38 passed in 46.42s
----- tests (python-version: 3.14) -----
python 3.14.7 | matplotlib 3.11.1 | numpy 2.5.2 | pytest 9.1.1
38 passed in 45.57s

----- cross-check: python 3.9 with matplotlib pinned to 3.7.5 -----
cross-check: python 3.9.25 with matplotlib 3.7.5
38 passed, 11 warnings in 31.03s

----- docs job (python-version: 3.14, pip install -e '.[docs]'; make -C docs html) -----
Running Sphinx v9.1.0
build succeeded.
The HTML pages are in _build/html.

----- docs: -W is in effect and does fail a warning -----
$ make -C docs -n html   ->   sphinx-build -M html "." "_build" -W
with a page left out of the toctree:
build finished with problems, 1 warning (with warnings treated as errors).
make: *** [html] Error 1

----- regression check: base commit 5c9d512 on python 3.8 / matplotlib 3.7.5 -----
FAILED tests/test_pyLEAFS.py::test_viewer_3d_fill_is_the_true_worst_case_over_all_view_angles
  np.allclose(array([0.9127639, 0.9888995]), (0.9526, 1.0302), atol=0.0005) -> False
same test at f2bf5a4 on the same interpreter: passed (fill measured as 0.91276 x 0.98890)
Evidence: setup-python manifest: 3.8/3.9 have no build newer than ubuntu-24.04
python 3.8 -> linux platform_versions published: ['16.04', '18.04', '20.04', '22.04', '24.04']
python 3.9 -> linux platform_versions published: ['16.04', '18.04', '20.04', '22.04', '24.04']
python 3.10 -> linux platform_versions published: ['16.04', '18.04', '20.04', '22.04', '24.04', '26.04']
python 3.14 -> linux platform_versions published: ['20.04', '22.04', '24.04', '26.04']
Evidence: Measured fill is a property of mplot3d alone (36 configurations, two matplotlib versions)
measured once, off-screen, on a unit box: 0.912764 x 0.988899 (matplotlib 3.7.5)
world shapes x figure sizes/dpi x axes rects checked: 36 combinations
largest deviation from the single off-screen measurement: 4.44e-16

measured once, off-screen, on a unit box: 0.952573 x 1.030104 (matplotlib 3.11.1)
world shapes x figure sizes/dpi x axes rects checked: 36 combinations
largest deviation from the single off-screen measurement: 3.33e-16

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed ✅
  • ⚠️ .github/workflows/tests.yml:9 - The matrix job pins runs-on: ubuntu-latest, but actions/python-versions publishes no ubuntu-26.04 builds for Python 3.8 or 3.9 (manifest coverage: 3.8/3.9 -> 16.04-24.04; 3.10+ -> up to 26.04), and neither will be added since both are EOL upstream. ubuntu-26.04 images already exist in preview and the runner-images README documents a gradual -latest migration to the newest stable image. When that migration reaches ubuntu-latest, the "3.8" and "3.9" legs fail at the setup-python step with "Version 3.8 with arch x64 not found" - silently losing exactly the 3.8 coverage this change expanded scope to preserve (the viewer/test fix exists only because 3.8 must stay tested). Suggested fix: pin the matrix job to runs-on: ubuntu-24.04 (the docs job on 3.14 can stay on ubuntu-latest). This is a runner-policy call rather than a mechanical fix, so it is flagged for the author rather than changed here.
  • ℹ️ .github/workflows/tests.yml:7 - No permissions: block, so both jobs receive the repository's default GITHUB_TOKEN scope (write-all on repos created before the default changed). Neither job needs anything beyond reading the checkout. Adding permissions:\n contents: read at the workflow level costs nothing and keeps the workflow as boring as intended - no new actions, no caching.
  • ℹ️ .github/workflows/tests.yml:32 - make -C docs html invokes sphinx-build without -W, and docs/Makefile passes SPHINXOPTS through empty by default, so Sphinx warnings (broken cross-references, missing toctree entries, autodoc import warnings) exit 0 and pass CI. The job therefore catches hard build failures - the majority case - but not silent documentation rot. SPHINXOPTS=-W make -C docs html would catch both; whether the manual should be warning-clean is the author's call, hence flagged rather than changed.
  • ℹ️ pyLEAFS/viewer.py:56 - The module-level _scene_fill_3d_cache sentinel plus the global statement and is None guard hand-roll a memoisation that functools.lru_cache(maxsize=None) on _scene_fill_3d() provides directly (available on Python 3.8; functools.cache is 3.9+, so it cannot be used given the 3.8 floor). That removes the module global, the global declaration, and one level of indentation from the body while keeping identical measure-once semantics.

🔧 Fix: Pin CI runner, add permissions, -W docs, lru_cache memo
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Local mirror of the workflow tests job on a fresh checkout per version: python$V -m venv, python -m pip install --upgrade pip, pip install -e '.[test]', pytest for 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14 - 38 passed on each
  • Cross-check pip install 'matplotlib==3.7.5' on Python 3.9 then pytest - 38 passed
  • Regression proof: base commit 5c9d512 on Python 3.8 / matplotlib 3.7.5, pytest -k fill - test_viewer_3d_fill_is_the_true_worst_case_over_all_view_angles FAILS (0.9127639, 0.9888995 vs hard-coded 0.9526, 1.0302); same test at f2bf5a4 passes
  • Local mirror of the workflow docs job on Python 3.14: pip install -e '.[docs]' then make -C docs html - build succeeded
  • make -C docs -n html shows sphinx-build -M html "." "_build" -W; adding a page outside the toctree gives 1 warning (with warnings treated as errors) and make: *** [html] Error 1
  • Parsed .github/workflows/tests.yml with PyYAML: triggers push+pull_request, fail-fast: false, matrix 3.8-3.14, only actions/checkout@v4 and actions/setup-python@v5, no cache config, permissions: contents: read
  • Cross-checked the matrix against requires-python, the Programming Language classifiers and the README "tested on 3.8 through 3.14" claim - all agree
  • Checked the actions/python-versions manifest: linux builds of 3.8/3.9 exist for ubuntu-24.04 but not 26.04, so the runner pin is required for those matrix entries
  • Independence probe of _scene_fill_3d() over 36 combinations of world shape x figure size/dpi x axes rectangle on matplotlib 3.7.5 and 3.11.1 - max deviation 4.44e-16
  • Mutation check: scaling the measured fill by 1.01 and 0.99 in a throwaway copy makes test_viewer_3d_fill_is_the_true_worst_case_over_all_view_angles (and, when too small, test_viewer_3d_box_stays_in_the_window_at_every_view_angle) fail; unmutated they pass
  • Manual render of the 3d Viewer at the worst-case view angle on matplotlib 3.7.5 (before and after) and 3.11.1 (after), annotated with the reserved scene rectangle and the drawn world box
⚠️ **Document** - 1 info
  • ℹ️ .github/workflows/tests.yml:1 - Judgment call, left unresolved on purpose: the repository now has CI (tests on 3.8-3.14 plus a docs build on every push and PR) but no document mentions it. Nothing existing became stale - README and docs/getting_started.rst already claim 3.8-3.14 support and publish exactly the commands the workflow runs - so there was no stale fact to fix. The conventional home would be a status badge in README.md or a CONTRIBUTING.md, but both are additions (one a new documentation surface) rather than staleness repairs, so they were deliberately not made here. Worth a follow-up if the author wants CI status visible.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@EternalTime
EternalTime merged commit 8fadecf into master Aug 19, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant