Skip to content

Commit ad85e2c

Browse files
Register PyVista dataset accessor (#114)
* Register PyVista dataset accessor * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 5c7e22c commit ad85e2c

3 files changed

Lines changed: 171 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ readme = "README.rst"
2727
requires-python = ">=3.10"
2828
version = "0.9.dev0"
2929

30+
[project.entry-points."pyvista.accessors"]
31+
# Value points at the plugin module; importing it runs the
32+
# ``@register_dataset_accessor`` decorator as a side effect.
33+
tetgen = "tetgen._accessor"
34+
3035
[project.optional-dependencies]
3136
docs = [
3237
"pyvista",

src/tetgen/_accessor.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Register a ``.tetgen`` accessor on :class:`pyvista.PolyData`.
2+
3+
Importing this module (which :mod:`tetgen` does on package import)
4+
attaches a :class:`TetGenAccessor` so every :class:`~pyvista.PolyData`
5+
instance exposes the ``.tetgen`` namespace.
6+
7+
Requires PyVista >= 0.48, which introduced ``register_dataset_accessor``.
8+
On older versions importing this module is a no-op.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from typing import TYPE_CHECKING
14+
from typing import Any
15+
16+
import pyvista as pv
17+
18+
if TYPE_CHECKING:
19+
from tetgen.pytetgen import TetGen
20+
21+
22+
HAS_ACCESSOR_REGISTRY = hasattr(pv, "register_dataset_accessor")
23+
24+
25+
def _register(cls):
26+
if HAS_ACCESSOR_REGISTRY:
27+
return pv.register_dataset_accessor("tetgen", pv.PolyData)(cls)
28+
return cls
29+
30+
31+
@_register
32+
class TetGenAccessor:
33+
"""Tetrahedralization accessor for surface meshes.
34+
35+
Wraps the :class:`tetgen.TetGen` tetrahedralizer so a PyVista user
36+
can call it directly on any :class:`~pyvista.PolyData` surface::
37+
38+
import pyvista as pv
39+
import tetgen # noqa: F401 — registers the ``.tetgen`` accessor
40+
41+
grid = pv.Sphere().tetgen.tetrahedralize(order=1, mindihedral=20)
42+
43+
The underlying :class:`~tetgen.TetGen` instance is constructed
44+
lazily on first use and cached. Access it via :attr:`instance`
45+
when you need the lower-level API (raw arrays, markers, edges).
46+
"""
47+
48+
def __init__(self, mesh: pv.PolyData) -> None:
49+
"""Bind the accessor to its parent :class:`~pyvista.PolyData`."""
50+
self._mesh = mesh
51+
self._tetgen: TetGen | None = None
52+
53+
@property
54+
def instance(self) -> TetGen:
55+
"""Return the underlying :class:`tetgen.TetGen` object.
56+
57+
Constructed lazily on first access. Use this for the raw array
58+
outputs (``node``, ``elem``, ``triface_markers``) or the
59+
``.make_manifold()`` preprocessor.
60+
"""
61+
if self._tetgen is None:
62+
from tetgen import TetGen # noqa: PLC0415
63+
64+
self._tetgen = TetGen(self._mesh)
65+
return self._tetgen
66+
67+
def tetrahedralize(self, **kwargs: Any) -> pv.UnstructuredGrid:
68+
"""Tetrahedralize the surface mesh and return the volume grid.
69+
70+
Forwards all keyword arguments to
71+
:meth:`tetgen.TetGen.tetrahedralize`. Returns the resulting
72+
:class:`~pyvista.UnstructuredGrid` directly so the call chains
73+
cleanly with PyVista filters::
74+
75+
pv.Sphere().tetgen.tetrahedralize(order=1).extract_cells([0, 1, 2])
76+
77+
See :meth:`tetgen.TetGen.tetrahedralize` for the full parameter
78+
list.
79+
"""
80+
self.instance.tetrahedralize(**kwargs)
81+
return self.instance.grid
82+
83+
def make_manifold(self, **kwargs: Any) -> pv.PolyData:
84+
"""Repair non-manifold input and return the cleaned surface.
85+
86+
Forwards to :meth:`tetgen.TetGen.make_manifold`. See that
87+
method's docstring for parameters.
88+
"""
89+
self.instance.make_manifold(**kwargs)
90+
return self.instance.mesh

tests/test_accessor.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Tests for the ``.tetgen`` accessor registered on :class:`pyvista.PolyData`."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
import pyvista as pv
7+
8+
import tetgen
9+
import tetgen._accessor # noqa: F401 — registers the ``.tetgen`` accessor
10+
11+
pytestmark = pytest.mark.skipif(
12+
not tetgen._accessor.HAS_ACCESSOR_REGISTRY,
13+
reason="requires pyvista >= 0.48 dataset accessor registry",
14+
)
15+
16+
17+
def test_accessor_attached_on_polydata():
18+
assert hasattr(pv.Sphere(), "tetgen")
19+
20+
21+
def test_accessor_not_attached_on_non_polydata():
22+
# Grid/ImageData types should not expose a surface-mesh accessor.
23+
assert not hasattr(pv.ImageData(), "tetgen")
24+
assert not hasattr(pv.UnstructuredGrid(), "tetgen")
25+
26+
27+
def test_accessor_cached_per_instance():
28+
sphere = pv.Sphere()
29+
assert sphere.tetgen is sphere.tetgen
30+
31+
32+
def test_tetrahedralize_returns_unstructured_grid():
33+
sphere = pv.Sphere(theta_resolution=10, phi_resolution=10)
34+
grid = sphere.tetgen.tetrahedralize(order=1, mindihedral=20, minratio=1.5)
35+
assert isinstance(grid, pv.UnstructuredGrid)
36+
assert grid.n_cells > 0
37+
assert grid.n_points > 0
38+
39+
40+
def test_tetrahedralize_matches_direct_api():
41+
sphere = pv.Sphere(theta_resolution=10, phi_resolution=10)
42+
direct = tetgen.TetGen(sphere)
43+
direct.tetrahedralize(order=1, mindihedral=20, minratio=1.5)
44+
direct_grid = direct.grid
45+
46+
accessor_grid = sphere.tetgen.tetrahedralize(order=1, mindihedral=20, minratio=1.5)
47+
assert accessor_grid.n_cells == direct_grid.n_cells
48+
assert accessor_grid.n_points == direct_grid.n_points
49+
50+
51+
def test_instance_lazy_and_cached():
52+
sphere = pv.Sphere()
53+
accessor = sphere.tetgen
54+
first = accessor.instance
55+
second = accessor.instance
56+
assert first is second
57+
assert isinstance(first, tetgen.TetGen)
58+
59+
60+
def test_chains_with_core_filters():
61+
"""Accessor result chains cleanly with a core PyVista filter."""
62+
grid = (
63+
pv.Sphere(theta_resolution=10, phi_resolution=10)
64+
.tetgen.tetrahedralize(order=1)
65+
.extract_cells([0, 1, 2])
66+
)
67+
assert isinstance(grid, pv.UnstructuredGrid)
68+
assert grid.n_cells == 3
69+
70+
71+
def test_registered_record_reports_tetgen_as_source():
72+
records = [r for r in pv.registered_accessors() if r.name == "tetgen"]
73+
assert len(records) == 1
74+
record = records[0]
75+
assert record.target is pv.PolyData
76+
assert record.source.startswith("tetgen._accessor")

0 commit comments

Comments
 (0)