From 730bb402a1225751a4deddf22e98b3d72ef1ea54 Mon Sep 17 00:00:00 2001 From: Steve Pieper Date: Thu, 30 Jul 2026 19:46:40 -0400 Subject: [PATCH 1/3] BUG: Import VTK dependency modules so wrapped subclasses keep inherited methods The Python module initialization generated by vtkMacroKitPythonWrap listed a library's Slicer dependencies in the init's vtkPythonUtil::ImportModule list but deliberately excluded its VTK dependencies. That worked only because importing "vtk" eagerly loaded every VTK module, so a wrapped subclass's VTK base class was always registered by the time its Python type was built. With lazy VTK loading (a "vtk" shim that imports vtkmodules submodules on demand), the VTK base module is no longer guaranteed to be loaded first. A wrapped subclass such as vtkOrientedGridTransform (whose base vtkGridTransform lives in vtkmodules.vtkFiltersHybrid) is then built with base "object" and silently loses every inherited method -- for example SetDisplacementGridData -- until something else happens to import the base module. Emit the VTK dependencies in the module init's import list too, mirroring VTK's own module wrapper (vtkModuleWrapPython): skip modules marked exclude_wrap and name the rest "." (e.g. vtkmodules.vtkFiltersHybrid). Each library wrapped with this macro -- vtkAddon, and every Slicer or extension library using it -- then imports its VTK base modules when it loads, so lazy VTK loading no longer breaks cross-module inheritance. Co-Authored-By: Claude Opus 4.8 --- CMake/vtkWrapPython.cmake | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/CMake/vtkWrapPython.cmake b/CMake/vtkWrapPython.cmake index 5eeb218..b6ff9d2 100644 --- a/CMake/vtkWrapPython.cmake +++ b/CMake/vtkWrapPython.cmake @@ -138,7 +138,31 @@ $<$>: if(NOT ${dep}_KIT STREQUAL kit_basename) list(APPEND _python_module_depends ${${dep}_KIT}KitPython) endif() - elseif(TARGET ${dep}Python AND NOT ${dep} MATCHES "VTK::") + elseif(dep MATCHES "^VTK::") + # Import the Python module of each VTK dependency so that base classes + # defined in VTK (for example vtkGridTransform in vtkFiltersHybrid) are + # registered before this library's wrapped subclasses are built. The + # eager "import vtk" used to load every VTK module up front; with lazy + # VTK loading it does not, so a wrapped subclass such as + # vtkOrientedGridTransform would otherwise be built without its VTK base + # class and silently lose the inherited methods. Follow VTK's own module + # wrapper (vtkModuleWrapPython): skip modules excluded from wrapping and + # name the rest ".". + _vtk_module_get_module_property("${dep}" + PROPERTY "exclude_wrap" + VARIABLE _dep_exclude_wrap) + if(NOT _dep_exclude_wrap) + _vtk_module_get_module_property("${dep}" + PROPERTY "python_package" + VARIABLE _dep_python_package) + _vtk_module_get_module_property("${dep}" + PROPERTY "library_name" + VARIABLE _dep_library_name) + if(_dep_python_package AND _dep_library_name) + list(APPEND _python_module_depends "${_dep_python_package}.${_dep_library_name}") + endif() + endif() + elseif(TARGET ${dep}Python) list(APPEND _python_module_depends ${dep}Python) endif() endforeach() From 193a054c37a5aadff8170ac3d992cf5491afb0fc Mon Sep 17 00:00:00 2001 From: Steve Pieper Date: Fri, 31 Jul 2026 10:13:21 -0400 Subject: [PATCH 2/3] PERF: Prune wrapped-module Python DEPENDS to base-class providers vtk_wrap_python seeds each wrapped module's Python dependencies from the whole VTK link line (${VTK_LIBRARIES}), so every "vtk*Init.data" gains a DEPENDS on all ~125 VTK Python modules. Importing a base class before its wrapped subclass is built only requires the module that *provides* that base (each VTK module imports its own bases transitively), so listing the full link line is both unnecessary and, with lazy VTK loading, harmful: importing any wrapped module would pull in all of VTK at startup. Add SlicerPrunePythonModuleDepends.py and run it as a build step: it reads the merged wrapping hierarchy (Class : Super ; header ; owning_module) and keeps only the vtkmodules.* DEPENDS whose module owns a direct base class of one of the module's wrapped classes, passing cross-module Slicer dependencies through unchanged. The pruning is conservative -- only spurious VTK modules are removed -- and falls back to the unpruned data file when the hierarchy file or a Python interpreter is unavailable. Applied across the tree the DEPENDS drop from 7223 to 520 entries over 54 modules with no module left empty. vtkAddonPython goes from 125 to 7 direct DEPENDS (16 vs 125 modules loaded transitively), while the inheritance fix is preserved: vtkOrientedGridTransform keeps its full method resolution order and SetDisplacementGridData. This makes the restored base-class DEPENDS coexist with lazy VTK loading instead of defeating it. Co-Authored-By: Claude Opus 4.8 --- CMake/SlicerPrunePythonModuleDepends.py | 154 ++++++++++++++++++++++++ CMake/vtkWrapPython.cmake | 30 ++++- 2 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 CMake/SlicerPrunePythonModuleDepends.py diff --git a/CMake/SlicerPrunePythonModuleDepends.py b/CMake/SlicerPrunePythonModuleDepends.py new file mode 100644 index 0000000..acf49b1 --- /dev/null +++ b/CMake/SlicerPrunePythonModuleDepends.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""Prune a wrapped module's Python Init.data DEPENDS to the modules it truly needs. + +Background +---------- +Slicer wraps its C++ classes for Python through vtkMacroKitPythonWrap, which +seeds each module's wrapping dependencies from the whole VTK link line +(``${VTK_LIBRARIES}``). The generated ``Init.data`` therefore lists a +``DEPENDS`` on every VTK python module -- 125 of them -- even though a module +such as vtkAddon only subclasses a handful of VTK base classes. + +That over-listing was harmless while ``import vtk`` eagerly loaded all of VTK. +With lazy VTK loading it is not: a wrapped subclass whose direct base class has +not been imported is built without that base and silently loses the inherited +methods (the vtkOrientedGridTransform.SetDisplacementGridData regression). The +fix restores the base-class imports -- but importing all 125 VTK modules at +startup defeats the point of lazy loading. + +A wrapped subclass only needs its **direct base class** registered first; each +VTK module in turn imports its own bases transitively. So the minimal, correct +set of VTK DEPENDS is exactly the modules that own the direct base classes of +the classes this module wraps. + +What this does +-------------- +Reads the class list and DEPENDS from an Init.data file and the merged wrapping +hierarchy (``Class : Super ; header ; owning_module``). Keeps every +``vtkmodules.X`` line whose X owns a direct base class of a wrapped class, drops +the rest, and passes all other DEPENDS lines (cross-module Slicer dependencies +such as MRMLCorePython) through unchanged. Conservative by construction: only +spurious VTK modules are removed. +""" +import argparse +import sys + + +def normalize(name): + """Reduce a hierarchy class token to a plain lookup key. + + Drops template parameters (``Foo`` -> ``Foo``) and rejects typedef-style + supers (``= something``) that are not real base classes. + """ + name = name.strip() + lt = name.find('<') + if lt != -1: + name = name[:lt] + if name.startswith('='): + return '' + return name.strip() + + +def parse_hierarchy(path): + """Return (super_of, module_of) mapping over every class in the file.""" + super_of = {} + module_of = {} + with open(path) as handle: + for line in handle: + line = line.strip() + if not line: + continue + fields = [part.strip() for part in line.split(';')] + if len(fields) < 3: + continue + decl, _header, module = fields[0], fields[1], fields[2] + if ' : ' in decl: + cls, sup = decl.split(' : ', 1) + else: + cls, sup = decl, None + cls = normalize(cls) + if not cls: + continue + module_of[cls] = module + if sup: + super_of[cls] = normalize(sup) + return super_of, module_of + + +def read_init_data(path): + """Split an Init.data file into (target, class_list, depends_list).""" + with open(path) as handle: + lines = [line.rstrip('\n') for line in handle] + target = lines[0] if lines else '' + classes = [] + depends = [] + in_depends = False + for line in lines[1:]: + if line.strip() == 'DEPENDS': + in_depends = True + continue + if in_depends: + if line.strip(): + depends.append(line.strip()) + else: + if line.strip(): + classes.append(line.strip()) + return target, classes, depends + + +def base_owner_modules(classes, super_of, module_of, own_target): + """Owning modules of the direct base classes of the wrapped classes.""" + owners = set() + for cls in classes: + sup = super_of.get(cls) + if not sup: + continue + owner = module_of.get(sup) + if owner and owner != own_target: + owners.add(owner) + return owners + + +def prune(init_data_path, hierarchy_path, output_path): + target, classes, depends = read_init_data(init_data_path) + super_of, module_of = parse_hierarchy(hierarchy_path) + owners = base_owner_modules(classes, super_of, module_of, target) + + kept = [] + dropped = [] + for dep in depends: + if dep.startswith('vtkmodules.'): + module = dep[len('vtkmodules.'):] + if module in owners: + kept.append(dep) + else: + dropped.append(dep) + else: + # Cross-module Slicer dependency (e.g. MRMLCorePython): keep as-is. + kept.append(dep) + + with open(output_path, 'w') as handle: + handle.write(target + '\n') + for cls in classes: + handle.write(cls + '\n') + if kept: + handle.write('DEPENDS\n') + for dep in kept: + handle.write(dep + '\n') + + sys.stderr.write( + '[prune-depends] %s: kept %d, dropped %d VTK module(s)\n' + % (target, len(kept), len(dropped))) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('init_data', help='input Init.data (full DEPENDS)') + parser.add_argument('hierarchy', help='merged Hierarchy.txt') + parser.add_argument('output', help='pruned Init.data to write') + args = parser.parse_args(argv) + prune(args.init_data, args.hierarchy, args.output) + + +if __name__ == '__main__': + main() diff --git a/CMake/vtkWrapPython.cmake b/CMake/vtkWrapPython.cmake index b6ff9d2..76f48da 100644 --- a/CMake/vtkWrapPython.cmake +++ b/CMake/vtkWrapPython.cmake @@ -183,6 +183,32 @@ $<$>: @ONLY ) + # The DEPENDS just written lists every VTK python module on the link line, + # because the wrapping dependencies are seeded from ${VTK_LIBRARIES}. Only the + # modules that own a direct base class of a wrapped class actually need to be + # imported first (each VTK module imports its own bases transitively). Prune + # the DEPENDS to that minimal set so lazy VTK loading is not defeated by + # importing all of VTK when this module is imported. Cross-module Slicer + # dependencies are preserved unchanged. Requires the merged hierarchy file and + # a Python interpreter; fall back to the unpruned data file when unavailable. + set(_init_data_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}Init.data") + if(KIT_HIERARCHY_FILE AND PYTHON_EXECUTABLE AND ${VTK_VERSION} VERSION_GREATER_EQUAL "8.90") + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}Init.pruned.data + DEPENDS ${vtkAddon_CMAKE_DIR}/SlicerPrunePythonModuleDepends.py + ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}Init.data + ${KIT_HIERARCHY_FILE} + COMMAND ${PYTHON_EXECUTABLE} + ${vtkAddon_CMAKE_DIR}/SlicerPrunePythonModuleDepends.py + ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}Init.data + ${KIT_HIERARCHY_FILE} + ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}Init.pruned.data + COMMENT "Pruning Python module dependencies for ${TARGET}" + VERBATIM + ) + set(_init_data_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}Init.pruned.data") + endif() + set(_init_impl_src "") if(${VTK_VERSION} VERSION_LESS "9.5.0") set(_init_impl_src "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}InitImpl.cxx") @@ -191,9 +217,9 @@ $<$>: OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}Init.cxx ${_init_impl_src} DEPENDS ${VTK_WRAP_PYTHON_INIT_EXE} - ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}Init.data + ${_init_data_file} COMMAND ${VTK_WRAP_PYTHON_INIT_EXE} - ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}Init.data + ${_init_data_file} ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}Init.cxx ${_init_impl_src} COMMENT "Generating the Python module initialization sources for ${TARGET}" From 98958afd703dac6f84e939d3dbf3946799f62799 Mon Sep 17 00:00:00 2001 From: Steve Pieper Date: Fri, 31 Jul 2026 11:41:07 -0400 Subject: [PATCH 3/3] BUG: Wire wrapping-hierarchy build-order dependencies generically A module's Python wrapping hierarchy must be generated after the hierarchies of the modules it wraps against. VTK_WRAP_HIERARCHY collected those as target-level dependencies (OTHER_HIERARCHY_TARGETS) only for hierarchy targets that already existed when the module was configured. A dependency configured *later* in the subdirectory order -- a forward reference, e.g. Transforms and Segmentations wrapping against Markups MRML -- had no "Hierarchy" target yet, so on the Makefiles generator that build-order edge was silently dropped and parallel builds could race with: vtkWrapHierarchy: couldn't open file .../vtkSlicerMarkupsModuleMRMLHierarchy.txt Incremental build trees masked it (the file was left over from a prior build); fresh parallel trees hit it depending on scheduling. Defer the dependency wiring with cmake_language(DEFER) to the end of the top-level directory, where every module has been configured and all hierarchy targets exist, and add each edge there (guarded on both targets existing). This handles forward and backward references uniformly and removes the need for projects to re-add known forward dependencies by hand after configuring all their modules. Co-Authored-By: Claude Opus 4.8 --- CMake/vtkWrapHierarchy.cmake | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CMake/vtkWrapHierarchy.cmake b/CMake/vtkWrapHierarchy.cmake index cd71b0a..f2fb262 100644 --- a/CMake/vtkWrapHierarchy.cmake +++ b/CMake/vtkWrapHierarchy.cmake @@ -1,6 +1,16 @@ # # a cmake macro to generate a text file with the class hierarchy # + +# Add a build-order dependency between two hierarchy targets if both exist. +# Used as a deferred call (see VTK_WRAP_HIERARCHY) so the dependency can be wired +# even when the dependee module is configured after the module that wraps it. +function(_vtkAddon_wrap_hierarchy_add_dependency target dependency) + if(TARGET "${target}" AND TARGET "${dependency}") + add_dependencies("${target}" "${dependency}") + endif() +endfunction() + macro(VTK_WRAP_HIERARCHY module_name OUTPUT_DIR SOURCES) if(NOT VTK_WRAP_HIERARCHY_EXE) if(TARGET vtkWrapHierarchy) @@ -169,4 +179,24 @@ $<$>: DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${module_name}Hierarchy.stamp.txt) + # Wire the build-order dependency on the hierarchy targets this module wraps + # against. When a dependency is configured *after* this module (a forward + # reference in the subdirectory order), its "${dep}Hierarchy" target does not + # exist yet when OTHER_HIERARCHY_TARGETS is built above, so on the Makefiles + # generator that build-order edge is silently lost and parallel builds can + # race, failing with "vtkWrapHierarchy: couldn't open file ...Hierarchy.txt". + # Defer the wiring to the end of the top-level directory, where every module + # has been configured and all hierarchy targets exist, so forward and backward + # references are handled uniformly (this makes per-project workarounds that + # re-add known forward dependencies unnecessary). + foreach(dep ${${module_name}_WRAP_DEPENDS}) + if(NOT "${module_name}" STREQUAL "${dep}") + if(NOT ${dep}_EXCLUDE_FROM_WRAPPING) + cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" + CALL _vtkAddon_wrap_hierarchy_add_dependency + "${module_name}Hierarchy" "${dep}Hierarchy") + endif() + endif() + endforeach() + endmacro()