diff --git a/Documentation/release/dev/python-numpy-integration.md b/Documentation/release/dev/python-numpy-integration.md new file mode 100644 index 0000000000000000000000000000000000000000..28e0fed873b35790343c7bbfdba1dd88b53414c7 --- /dev/null +++ b/Documentation/release/dev/python-numpy-integration.md @@ -0,0 +1,13 @@ +## Add Python logic to enable module import at vtk module load + +This feature is mostly driven by the @override capability in Python to automatically enhance native vtk class with some Python one. + +By default we have added those following dependencies: +- vtkCommonDataModel: vtkmodules.util.data_model +- vtkCommonExecutionModel: vtkmodules.util.execution_model + +But now a user is able to add to it by calling `vtkmodules.register_vtk_module_dependencies(vtk_module_name, *import_strings)` to automate imports at vtk module loading. + +## Make numpy optional for vtkmodules.util.data_model + +`vtkmodules.util.data_model` has been added to enhance vtkDataModel API for Python using the @override infrastructure to mainly handle numpy in/out manipulation. But since numpy is an optional dependency for VTK, we are providing a downgraded version when numpy is not available so we can keep automatically load it at module startup regardless of numpy presence. diff --git a/Wrapping/Python/vtkmodules/__init__.py.in b/Wrapping/Python/vtkmodules/__init__.py.in index 25bf15447e8fc801c39766cd40618b1a2b534106..42bcce62f8b57751bcd7cf553edc14a0fa04078b 100644 --- a/Wrapping/Python/vtkmodules/__init__.py.in +++ b/Wrapping/Python/vtkmodules/__init__.py.in @@ -3,6 +3,51 @@ Currently, this package is experimental and may change in the future. """ from __future__ import absolute_import import sys +import importlib.util + +from pathlib import Path + +from importlib.abc import MetaPathFinder +from importlib.machinery import ExtensionFileLoader + +LOADING_STACK = [] + +def find_lib_path(paths, vtk_module_name): + for ext in [".pyd", ".so"]: + for base_path in paths: + for f in Path(base_path).glob(f"{vtk_module_name}.*{ext}"): + return str(f.resolve()) + + +class VTKMetaHook(MetaPathFinder): + """Attach a custom loaded for vtk native library loading to defer loading of pure python dependencies""" + def find_spec(self, fullname, path, target=None): + if fullname.startswith("vtkmodules.vtk"): + vtk_module_name = fullname.split(".")[1] + module_path = find_lib_path(path, vtk_module_name) + if module_path is None: + return None + + LOADING_STACK.append(fullname) + return importlib.util.spec_from_file_location(fullname, module_path, loader=VTKLoader(fullname, module_path)) + + return None + + +class VTKLoader(ExtensionFileLoader): + """Flush any pending dependency load once initialize() phase is done""" + def exec_module(self, module): + super().exec_module(module) + + # Process pending dependencies only if the module match the first load request + if len(LOADING_STACK) and LOADING_STACK[0] == module.__name__: + LOADING_STACK.clear() + on_vtk_module_init_completed() + + + +# Register our hook for vtk library loader +sys.meta_path.insert(0, VTKMetaHook()) def _windows_dll_path(): @@ -58,8 +103,48 @@ def _load_vtkmodules_static(): #------------------------------------------------------------------------------ # list the contents __all__ = [ -@_vtkmodules_all@] - + @_vtkmodules_all@ +] #------------------------------------------------------------------------------ # get the version __version__ = "@VTK_MAJOR_VERSION@.@VTK_MINOR_VERSION@.@VTK_BUILD_VERSION@" + +#------------------------------------------------------------------------------ +# describe import dependencies to properly define Python @override +MODULE_MAPPER = { + "vtkCommonDataModel": [ + "vtkmodules.util.data_model", + ], + "vtkCommonExecutionModel": [ + "vtkmodules.util.execution_model", + ], +} +LOADED_MODULES = set() +PENDING_LOADED_MODULES = set() + +def register_vtk_module_dependencies(vtk_module_name, *import_names): + """Method to call for registering external override on vtkmodule load""" + MODULE_MAPPER.setdefault(vtk_module_name, []).extend(import_names) + + # If already loaded let's make sure we import it now + if vtk_module_name in LOADED_MODULES: + for import_name in import_names: + importlib.import_module(import_name) + + +def on_vtk_module_init(module_name): + """Automatically called by vtkmodule when they are loaded""" + if module_name in LOADED_MODULES: + return + + PENDING_LOADED_MODULES.add(module_name) + + +def on_vtk_module_init_completed(): + pending = list(PENDING_LOADED_MODULES) + PENDING_LOADED_MODULES.clear() + + for module_name in pending: + LOADED_MODULES.add(module_name) + for import_name in MODULE_MAPPER.get(module_name, []): + importlib.import_module(import_name) diff --git a/Wrapping/Python/vtkmodules/util/data_model.py b/Wrapping/Python/vtkmodules/util/data_model.py index e83a6cf4a3e118a2a6cf480691037abd6f3fce19..d5553afd01f862ab6742c3c92d473fda6913b5af 100644 --- a/Wrapping/Python/vtkmodules/util/data_model.py +++ b/Wrapping/Python/vtkmodules/util/data_model.py @@ -1,8 +1,8 @@ """This module provides classes that allow numpy style access to VTK datasets. See examples at bottom. """ - -from vtkmodules.vtkCommonCore import vtkPoints +from contextlib import suppress +from vtkmodules.vtkCommonCore import vtkPoints, vtkAbstractArray from vtkmodules.vtkCommonDataModel import ( vtkCellArray, vtkDataObject, @@ -17,10 +17,15 @@ from vtkmodules.vtkCommonDataModel import ( vtkPartitionedDataSet ) -from vtkmodules.numpy_interface import dataset_adapter as dsa -import numpy import weakref +NUMPY_AVAILABLE = False + +with suppress(ImportError): + import numpy + from vtkmodules.numpy_interface import dataset_adapter as dsa + NUMPY_AVAILABLE = True + class FieldDataBase(object): def __init__(self): self.association = None @@ -39,6 +44,10 @@ class FieldDataBase(object): if isinstance(idx, int) and idx >= self.GetNumberOfArrays(): raise IndexError("array index out of range") vtkarray = super().GetArray(idx) + + if not NUMPY_AVAILABLE: + return vtkarray if vtkarray else self.GetAbstractArray(idx) + if not vtkarray: vtkarray = self.GetAbstractArray(idx) if vtkarray: @@ -70,6 +79,12 @@ class FieldDataBase(object): def set_array(self, name, narray): """Appends a new array to the dataset attributes.""" + if not NUMPY_AVAILABLE: + if isinstance(narray, vtkAbstractArray): + narray.SetName(name) + self.AddArray(narray) + return + if narray is dsa.NoneArray: # if NoneArray, nothing to do. return @@ -221,6 +236,10 @@ class CompositeDataSetAttributes(object): def set_array(self, name, narray): """Appends a new array to the composite dataset attributes.""" + if not NUMPY_AVAILABLE: + # don't know how to handle composite dataset attribute when numpy not around + raise NotImplementedError("Only available with numpy") + if narray is dsa.NoneArray: # if NoneArray, nothing to do. return @@ -246,6 +265,11 @@ class CompositeDataSetAttributes(object): def get_array(self, idx): """Given a name, returns a VTKCompositeArray.""" arrayname = idx + + if not NUMPY_AVAILABLE: + # don't know how to handle composite dataset attribute when numpy not around + raise NotImplementedError("Only available with numpy") + if arrayname not in self.ArrayNames: return dsa.NoneArray if arrayname not in self.Arrays or self.Arrays[arrayname]() is None: @@ -291,12 +315,23 @@ class PointSet(DataSet): @property def points(self): pts = self.GetPoints() + + if not NUMPY_AVAILABLE: + return pts + if not pts or not pts.GetData(): return None return dsa.vtkDataArrayToVTKArray(pts.GetData()) @points.setter def points(self, points): + if isinstance(points, vtkPoints): + self.SetPoints(points) + return + + if not NUMPY_AVAILABLE: + raise ValueError("Expect vtkPoints") + pts = dsa.numpyTovtkDataArray(points, "points") vtkpts = vtkPoints() vtkpts.SetData(pts) @@ -308,20 +343,33 @@ class vtkUnstructuredGrid(PointSet, vtkUnstructuredGrid): def cells(self): ca = self.GetCells() conn_vtk = ca.GetConnectivityArray() - conn = dsa.vtkDataArrayToVTKArray(conn_vtk) offsets_vtk = ca.GetOffsetsArray() - offsets = dsa.vtkDataArrayToVTKArray(offsets_vtk) ct_vtk = self.GetCellTypesArray() + + if not NUMPY_AVAILABLE: + return { + 'connectivity' : conn_vtk, + 'offsets' : offsets_vtk, + 'cell_types' : ct_vtk, + } + + conn = dsa.vtkDataArrayToVTKArray(conn_vtk) + offsets = dsa.vtkDataArrayToVTKArray(offsets_vtk) ct = dsa.vtkDataArrayToVTKArray(ct_vtk) return { 'connectivity' : conn, 'offsets' : offsets , 'cell_types' : ct} @cells.setter def cells(self, cells): ca = vtkCellArray() + + if not NUMPY_AVAILABLE: + ca.SetData(cells['offsets'], cells['connectivity']) + self.SetCells(cells['cell_types'], ca) + return + conn_vtk = dsa.numpyTovtkDataArray(cells['connectivity']) offsets_vtk = dsa.numpyTovtkDataArray(cells['offsets']) cell_types_vtk = dsa.numpyTovtkDataArray(cells['cell_types']) - print(cells['cell_types'][1]) ca.SetData(offsets_vtk, conn_vtk) self.SetCells(cell_types_vtk, ca) @@ -335,8 +383,15 @@ class vtkPolyData(PointSet, vtkPolyData): def polygons(self): ca = self.GetPolys() conn_vtk = ca.GetConnectivityArray() - conn = dsa.vtkDataArrayToVTKArray(conn_vtk) offsets_vtk = ca.GetOffsetsArray() + + if not NUMPY_AVAILABLE: + return { + 'connectivity' : conn_vtk, + 'offsets' : offsets_vtk, + } + + conn = dsa.vtkDataArrayToVTKArray(conn_vtk) offsets = dsa.vtkDataArrayToVTKArray(offsets_vtk) return { 'connectivity' : conn, 'offsets' : offsets } @@ -406,7 +461,7 @@ class CompositeDataSetBase(object): def cell_data(self): "Returns the cell data as a DataSetAttributes instance." if self._CellData is None or self._CellData() is None: - cdata = self.get_attributes(DataObject.CELL) + cdata = self.get_attributes(vtkDataObject.CELL) self._CellData = weakref.ref(cdata) return self._CellData() @@ -414,13 +469,17 @@ class CompositeDataSetBase(object): def field_data(self): "Returns the field data as a DataSetAttributes instance." if self._FieldData is None or self._FieldData() is None: - fdata = self.get_attributes(DataObject.FIELD) + fdata = self.get_attributes(vtkDataObject.FIELD) self._FieldData = weakref.ref(fdata) return self._FieldData() @property def points(self): "Returns the points as a VTKCompositeDataArray instance." + if not NUMPY_AVAILABLE: + # don't know how to handle composite dataset when numpy not around + raise NotImplementedError("Only available with numpy") + if self._Points is None or self._Points() is None: pts = [] for ds in self: @@ -430,11 +489,11 @@ class CompositeDataSetBase(object): _pts = None if _pts is None: - pts.append(NoneArray) + pts.append(dsa.NoneArray) else: pts.append(_pts) - if len(pts) == 0 or all([a is NoneArray for a in pts]): - cpts = NoneArray + if len(pts) == 0 or all([a is dsa.NoneArray for a in pts]): + cpts = dsa.NoneArray else: cpts = dsa.VTKCompositeDataArray(pts, dataset=self) self._Points = weakref.ref(cpts) @@ -444,3 +503,15 @@ class CompositeDataSetBase(object): class vtkPartitionedDataSet(CompositeDataSetBase, vtkPartitionedDataSet): def append(self, dataset): self.SetPartition(self.GetNumberOfPartitions(), dataset) + +# ----------------------------------------------------------------------------- +# Handle pickle registration +# ----------------------------------------------------------------------------- +with suppress(ImportError): + import copyreg + from vtkmodules.util.pickle_support import serialize_VTK_data_object + + copyreg.pickle(vtkPolyData, serialize_VTK_data_object) + copyreg.pickle(vtkUnstructuredGrid, serialize_VTK_data_object) + copyreg.pickle(vtkImageData, serialize_VTK_data_object) + copyreg.pickle(vtkPartitionedDataSet, serialize_VTK_data_object) diff --git a/Wrapping/Python/vtkmodules/util/pickle_support.py b/Wrapping/Python/vtkmodules/util/pickle_support.py index 6abb472778c722a64bccb3dde5d9429f2d4aa8c5..8fcbf2d958d939d99fa64890afde79a4e0d8d979 100644 --- a/Wrapping/Python/vtkmodules/util/pickle_support.py +++ b/Wrapping/Python/vtkmodules/util/pickle_support.py @@ -22,7 +22,7 @@ objects in the global dispatch table used by pickle. NumPy is required as well s try: import copyreg, pickle, numpy except ImportError: - raise RuntimeError("This module depends on the pickle, copyreg, and numpy modules.\ + raise ImportError("This module depends on the pickle, copyreg, and numpy modules.\ Please make sure that it is installed properly.") from ..vtkParallelCore import vtkCommunicator diff --git a/Wrapping/PythonCore/vtkPythonUtil.cxx b/Wrapping/PythonCore/vtkPythonUtil.cxx index f401af3668b43ad0a9d7b01dba594725f1d8cc1e..66631b58b5933b21a6b25b838029e06ceb7ec36a 100644 --- a/Wrapping/PythonCore/vtkPythonUtil.cxx +++ b/Wrapping/PythonCore/vtkPythonUtil.cxx @@ -1041,6 +1041,17 @@ bool vtkPythonUtil::ImportModule(const char* fullname, PyObject* globals) void vtkPythonUtil::AddModule(const char* name) { vtkPythonMap->ModuleList->push_back(name); + + // Register module name into pending list for defered side module loading + PyObject* pModule = PyImport_ImportModule("vtkmodules"); + PyObject* pFunc = PyObject_GetAttrString(pModule, "on_vtk_module_init"); + PyObject* pArgs = PyTuple_New(1); + PyTuple_SetItem(pArgs, 0, PyUnicode_FromString(name)); + PyObject* execVal = PyObject_CallObject(pFunc, pArgs); + Py_DECREF(execVal); + Py_DECREF(pArgs); + Py_DECREF(pFunc); + Py_DECREF(pModule); } //------------------------------------------------------------------------------