Skip to content
Open
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
20 changes: 12 additions & 8 deletions .github/scripts/strip_blender_wheel_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
"""Derive a slim "core" variant of an mmgpy wheel for the Blender bundle.

Rewrites the wheel's ``*.dist-info/METADATA`` in place to drop runtime deps
the Blender extension code path does not reach (``pyvista``, ``scipy``,
``rich``, ``patchelf``, ``typing-extensions``) and to widen the ``numpy``
floor so Blender 4.2 LTS (numpy 1.26) is covered alongside Blender 5.0
(numpy 2.x). The compiled ``_mmgpy.so`` and every Python module in the
wheel are left untouched — this is a METADATA-only strip.
the Blender extension code path does not reach (``rich``, ``patchelf``) and
to widen the ``numpy`` floor so Blender 4.2 LTS (numpy 1.26) is covered
alongside Blender 5.0 (numpy 2.x). The compiled ``_mmgpy.so`` and every
Python module in the wheel are left untouched, this is a METADATA-only
strip.

``pyvista`` and ``scipy`` are no longer in the published wheel's
``Requires-Dist`` (both moved to extras / opt-in installs), so they no
longer need stripping. ``typing-extensions`` is environment-conditional on
``python_version < '3.11'`` and never activates on Blender's Python
3.11 / 3.13.

The PyPI ``mmgpy`` wheel keeps its full ``Requires-Dist``; only the copy
that ends up inside the Blender extension zip is rewritten.
Expand All @@ -31,9 +37,7 @@

# Hard-coded by design: this script encodes the Blender add-on's contract
# with mmgpy, not a general-purpose wheel surgery tool.
STRIP_DEPS: frozenset[str] = frozenset(
{"pyvista", "scipy", "rich", "patchelf", "typing-extensions"},
)
STRIP_DEPS: frozenset[str] = frozenset({"rich", "patchelf"})
# Blender 4.2 LTS ships numpy 1.26.x; Blender 5.0 ships numpy 2.x.
# mmgpy's compiled extension is built against numpy's stable ABI so both
# work at runtime; the bundled METADATA just needs to advertise that.
Expand Down
20 changes: 12 additions & 8 deletions .github/workflows/build-blender-extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,19 +176,23 @@ jobs:
*) echo "Unexpected wheel: ${wheels[0]}" >&2; exit 1 ;;
esac

- name: Strip pyvista/scipy/rich from the bundled wheel METADATA
- name: Strip rich/patchelf from the bundled wheel METADATA
working-directory: blender_mmgpy/wheels
shell: bash
run: |
set -euo pipefail
# The Blender extension never imports the lazy pyvista- or
# scipy-coupled paths, so the bundled mmgpy wheel's METADATA
# ``Requires-Dist`` is rewritten to drop those deps. The numpy
# floor is also widened from ``>=2.0.2`` to ``>=1.26`` so the
# extension can install on Blender 4.2 LTS (which ships numpy
# 1.26.x) as well as Blender 5.0 (numpy 2.x). The compiled
# The Blender extension never reaches mmgpy's rich-formatted CLI
# paths and doesn't run the Linux RPATH fixer (manylinux wheels
# have RPATH baked in already), so the bundled mmgpy wheel's
# METADATA ``Requires-Dist`` is rewritten to drop those two deps.
# The numpy floor is also widened from ``>=2.0.2`` to ``>=1.26``
# so the extension can install on Blender 4.2 LTS (which ships
# numpy 1.26.x) as well as Blender 5.0 (numpy 2.x). The compiled
# ``_mmgpy.so`` and every Python module in the wheel are left
# untouched -- this is a METADATA-only strip.
# pyvista and scipy are no longer in the published wheel's
# Requires-Dist (both opt-in via extras / direct install), so
# they don't need stripping.
for wheel in mmgpy-*.whl; do
python ../../.github/scripts/strip_blender_wheel_metadata.py "$wheel"
done
Expand All @@ -204,7 +208,7 @@ jobs:
echo "FAIL: no METADATA found in $wheel" >&2
exit 1
fi
for forbidden in pyvista scipy rich patchelf typing-extensions; do
for forbidden in rich patchelf; do
if echo "$metadata" | grep -qE "^Requires-Dist:\\s*${forbidden}(\\W|$)"; then
echo "FAIL: $wheel still requires $forbidden" >&2
echo "$metadata" | grep -E "^Requires-Dist:" >&2
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,19 @@ tris_out = mesh.get_triangles()

`MmgMesh2D` (planar triangular) and `MmgMesh3D` (tetrahedral) follow the same pattern. File-based round trips are also available without pyvista via `mmgpy.mmg2d.remesh(in_path, out_path, options={...})` and its `mmg3d` / `mmgs` siblings.

### Features that need SciPy

A few helpers reach for `scipy` (sparse adjacency, KD-trees, sparse linear solves) and are not installed by `pip install mmgpy`. `pip install scipy` separately to unlock them:

- `mmgpy.move_mesh`, `mmgpy.propagate_displacement`, `mmgpy.detect_boundary_vertices` (lagrangian motion)
- `mmgpy.transfer_fields`, `mmgpy.interpolate_field` (field transfer between meshes)
- `mmgpy.repair` (duplicate-vertex repair via KD-tree)
- `mmgpy.ValidationReport` and the rest of `mmgpy.IssueSeverity` / `QualityStats` / `ValidationError` / `ValidationIssue`
- `mmgpy.reorder_cuthill_mckee`
- `mmgpy.metrics.compute_hessian` (least-squares Hessian recovery from a scalar field)

`import mmgpy` still works without scipy; access raises `ImportError` only when you reach one of these names. The rest of `mmgpy.metrics` (`create_isotropic_metric`, `create_anisotropic_metric`, tensor conversions, validation, intersection) is pure numpy and works on the slim install.

### Using uv for project management

```bash
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ dependencies = [
"numpy>=2.0.2,<3",
"patchelf>=0.17.2.4,<1; sys_platform == 'linux'",
"rich>=13.0.0,<15",
"scipy>=1.11.0,<2",
"typing-extensions>=4.0.0,<5; python_version < '3.11'",
]
authors = [{ name = "Kevin Marchais", email = "kevinmarchais@gmail.com" }]
Expand Down Expand Up @@ -48,6 +47,7 @@ pyvista = [
ui = [
"mmgpy[pyvista]",
"pywebview>=6.1,<7",
"scipy>=1.11.0,<2",
"trame>=3.12.0,<4",
"trame-vtk>=2.10.2,<3",
"trame-vtklocal>=0.16.0,<1",
Expand Down Expand Up @@ -125,6 +125,7 @@ dev = [
"pytest-codeblocks>=0.17.0",
"pytest-pyvista>=0.2",
"pyvista>=0.48,<1",
"scipy>=1.11.0,<2",
]
docs = [
"mike @ git+https://github.com/squidfunk/mike.git@0f62791256ebeba60d20d2f1d8fe6ec3b7d1e2b3",
Expand Down
6 changes: 4 additions & 2 deletions src/mmgpy/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@

import numpy as np

from mmgpy._topology import two_ring_patches, vertex_adjacency

if TYPE_CHECKING:
from collections.abc import Callable

Expand Down Expand Up @@ -675,6 +673,10 @@ def compute_hessian(
msg = f"field length {len(field)} != n_vertices {n_vertices}"
raise ValueError(msg)

# `_topology` depends on scipy.sparse; import it lazily so the rest of
# this module (pure numpy) stays usable on the slim install.
from mmgpy._topology import two_ring_patches, vertex_adjacency # noqa: PLC0415

# Quadratic fit has 5 monomials in 2D and 9 in 3D. The 1-ring around a
# boundary vertex on a structured grid often spans only 2 distinct values
# along an axis, which makes the linear and quadratic columns colinear and
Expand Down
10 changes: 6 additions & 4 deletions tests/headless_subset_test.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Regression test for the heavy-dep-free import subset.

The Blender add-on ships mmgpy without bundling pyvista/vtk/scipy/matplotlib
wheels. This test pins the contract that ``import mmgpy`` plus access to the
headless public API does not pull any of those heavy deps into
``sys.modules``. The heavy deps load only on first access to the lazy names
exposed via the module's ``__getattr__``.
wheels. ``pyvista`` and ``scipy`` are both opt-in for end users too (pyvista
via the ``[pyvista]`` extra, scipy via a plain ``pip install scipy``); this
test pins the contract that ``import mmgpy`` plus access to the headless
public API does not pull any of those heavy deps into ``sys.modules``. The
heavy deps load only on first access to the lazy names exposed via the
module's ``__getattr__``.

Runs in a subprocess because ``tests/conftest.py`` imports pyvista at
collection time, so any in-process check would observe it already loaded.
Expand Down
30 changes: 20 additions & 10 deletions tests/strip_blender_wheel_metadata_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,30 @@ def _load_module() -> ModuleType:


def test_rewrite_metadata_strips_heavy_deps() -> None:
"""Each forbidden dep is removed from ``Requires-Dist``."""
"""``rich`` and ``patchelf`` are removed from ``Requires-Dist``."""
result = strip_mod.rewrite_metadata(METADATA_INPUT)
for forbidden in ("pyvista", "scipy", "rich", "patchelf", "typing-extensions"):
for forbidden in ("rich", "patchelf"):
assert f"Requires-Dist: {forbidden}" not in result, forbidden


def test_rewrite_metadata_preserves_non_strip_deps() -> None:
"""Deps that aren't in ``STRIP_DEPS`` (pyvista/scipy/typing-extensions) survive.

pyvista and scipy are no longer in the published wheel's Requires-Dist
anyway (both opt-in via extras / direct install). typing-extensions is
environment-conditional on ``python_version < '3.11'``. None of them
needs stripping by this script, and stripping unknown deps would be a
bug, so we explicitly pin the surviving-line contract.
"""
result = strip_mod.rewrite_metadata(METADATA_INPUT)
for kept in (
"Requires-Dist: pyvista<1,>=0.48",
"Requires-Dist: scipy<2,>=1.11.0",
'Requires-Dist: typing-extensions<5,>=4.0.0; python_version < "3.11"',
):
assert kept in result, kept


def test_rewrite_metadata_loosens_numpy() -> None:
"""The numpy pin is replaced with the Blender-friendly ``>=1.26,<3`` range."""
result = strip_mod.rewrite_metadata(METADATA_INPUT)
Expand Down Expand Up @@ -105,14 +123,6 @@ def test_rewrite_metadata_raises_when_numpy_missing() -> None:
strip_mod.rewrite_metadata(no_numpy + "\n")


def test_rewrite_metadata_normalises_underscores() -> None:
"""``typing_extensions`` is matched against the canonical PEP 503 spelling."""
input_text = METADATA_INPUT.replace("typing-extensions", "typing_extensions")
result = strip_mod.rewrite_metadata(input_text)
assert "typing_extensions" not in result
assert "typing-extensions" not in result


def _b64_sha256(data: bytes) -> str:
return (
"sha256="
Expand Down
11 changes: 7 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading